Tehnički članak

Linearizovan PDF izlaz u Delphi: HotPDF hint tabele

HotPDF piše linearizovane PDF fajlove, raspored koji Acrobat naziva Fast Web View, kroz svojstvo LinearizeOutput na THotPDF. Postavljanje pre BeginDoc nateruje HotPDF da preuredi gotov graf objekata tako da čitač svestan opsega bajtova može prikazati prvu stranicu posle preuzimanja samo prednjeg dela fajla, umesto preuzimanja celog dokumenta prvo. Mehanizam je ISO 32000-1 Aneks F

Razlog zašto je ovo bitno je nespektakularan. Normalan PDF stavlja svoju cross-reference tabelu na kraj, tako da čitač mora dosegnuti poslednji bajt pre nego što zna gde se šta nalazi. Dajte browser-u skenirani izveštaj od 200 stranica i korisnik gleda u vrteći indikator tokom celog prenosa, iako je jedino što je hteo bila stranica 1. Linearizacija to popravlja plaćanjem cene u trenutku pisanja. Ovaj članak se konkretno bavi tom putanjom pisanja, podelom, petljom merenja i tvrdim ograničenjima; za konceptualnu pozadinu o tome šta vam Fast Web View donosi, raniji objašnjenje PDF linearizacije i Fast Web View pokriva to tlo

Šta linearizovan raspored zapravo garantuje

Linearizovan fajl je običan PDF sa izuzetno specifičnim fizičkim redosledom, i svaka garancija koju nudi dolazi iz tog redosleda, a ne iz bilo kog novog tipa objekta. HotPDF emituje delove u redosledu koji Aneks F propisuje: rečnik parametara linearizacije unutar prvih 1024 bajta, rani cross-reference tabela, objekti na nivou dokumenta, primarni hint tok, prva stranica i njeni privatni objekti, zatim preostale stranice, zatim deljeni objekti, zatim sve ostalo, i na kraju glavna cross-reference tabela

Podela je izvedena, ne deklarisana. HotPDF obilazi graf referenci od svakog objekta stranice i beleži, za svaki indirektni objekat, koliko stranica ga dostiže i koja stranica ga je prva dostigla. Objekat koji koristi tačno jedna stranica postaje privatan za tu stranicu. Objekat dostupan iz više njih postaje deljen. Katalog, plus sve što referencira pod /ViewerPreferences, /OpenAction, /Threads i /AcroForm, plus rečnik šifrovanja kad je zaštita aktivna, čine grupu na nivou dokumenta koja mora prethoditi svemu ostalom. Čvorovi stabla stranica se namerno zadržavaju kako ne bi zagadili odeljak prve stranice

Rečnik parametara nosi brojeve koji su čitaču potrebni pre nego što bilo šta drugo pročita: /L za ukupnu dužinu fajla, /H za offset i dužinu hint toka, /O za broj objekta prve stranice, /E za bajt gde se odeljak prve stranice završava, /N za broj stranica i /T za offset unosa glavne cross-reference tabele. Svaki od njih je offset bajta u fajl koji u trenutku kad ih morate pisati još ne postoji

Zašto offseti hint tabele moraju konvergirati?

Zato što brojevi u rečniku parametara opisuju fajl koji ih sadrži, i menjanje bilo kog od njih menja fajl. To je centralna teškoća linearizovanog pisača, i zbog toga HotPDF meri ponovljeno umesto da piše jednom. Proširite /T sa 6 cifara na 7 i rečnik parametara raste za bajt; zaglavlje raste; svaki objekat se pomera; glavna cross-reference tabela se pomera; /T sada treba drugačiju vrednost. Raspored mora dostići fiksnu tačku pre nego što se ijedan bajt stvarnog izlaza obaveže

HotPDF ovo rešava ograničenom iteracijom. Prvo serijalizuje svaki objekat u tok za brojanje koji beleži dužinu bez čuvanja bajtova, tako da svaki objekat ima poznatu serijalizovanu veličinu. Zatim pokreće prolaz rasporeda koji dodeljuje offsete grupi na nivou dokumenta, hint toku, grupi prve stranice, grupama kasnijih stranica, deljenoj grupi i ostatku, i prijavljuje gde bi glavna cross-reference tabela sletela. Taj rezultat se vraća nazad kao ulaz za sledeći prolaz. Petlja je ograničena na osam pokušaja, a nekonvergencija izaziva izuzetak umesto proizvodnje fajla sa uverljivim, ali pogrešnim offsetima

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

Dva detalja sprečavaju petlju od trzanja. Rečnik parametara se piše u fiksan slot od 384 bajta, popunjen razmacima, tako da sopstveni rast nikad ne može destabilizovati raspored; ako bi tekst rečnika ikad prekoračio tu rezervaciju, HotPDF izaziva izuzetak umesto da tiho pomera sve. A posle konvergencije HotPDF pokreće još jedan potvrđujući prolaz rasporeda i ponovo proverava dužinu hint toka, jer sam hint tok kodira offsete koji su bili poznati tek kad se raspored slegao. Isplata svih ovih merenja je da HotPDF nikad ne bafuje drugu kopiju dokumenta: kad su offseti fiksirani, objekti se serijalizuju direktno u odredišni tok, sa tvrdnjom na svakoj granici odeljka da napisani bajtovi odgovaraju obećanom offsetu

Uključivanje iz Delphi

Površina API-ja je jedan Boolean, a jedini zahtev je da ga postavite pre nego što generisanje počne. LinearizeOutput je podrazumevano False, a prolaz rasporeda se izvodi kad se dokument piše, tako da dodela posle EndDoc ne postiže ništa

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;

Jedno upozorenje o raspoređivanju nadmašuje sve na strani koda. Linearizacija se isplati samo kad transport podržava HTTP zahteve za opseg. Poslužite isti fajl sa endpoint-a koji ga strimuje ceo, ili sa CDN konfiguracije koja ignoriše Range, i kupili ste sebi sporiju putanju pisanja i veći fajl bez ikakve koristi vidljive korisniku. Proverite server pre nego što proverite kod

Zašto linearizacija preglasuje UseXRefStream i UseObjectStreams?

Zato što linearizovanom pisaču treba da svaki objekat ima sopstveni direktno adresibilan offset bajta, a obe te funkcije to oduzimaju. HotPDF zato emituje tradicionalne tekstualne cross-reference tabele i raspakovane indirektne objekte kad god je LinearizeOutput uključen, čak i ako je pozivalac takođe postavio UseXRefStream ili UseObjectStreams. Ovo je namerno preglasavanje, ne konflikt koji morate sami razrešiti

Rezonovanje sledi iz hint tabela. Hint tabela opisuje gde odeljak stranice počinje i koliko je dug, tako da čitač može zatražiti tačno taj opseg. Objekat pakovan u kontejner /ObjStm nema nikakav nezavisan offset; postoji samo kao isečak unutar drugog komprimovanog toka koji se mora preuzeti i napumpati kao celina. Ako ste računali na tokove objekata za veličinu fajla, znajte da linearizacija i kompresija ovde vuku u suprotnim pravcima, i pročitajte taj kompromis u pratećem tekstu o object streams i inkrementalnim ažuriranjima u HotPDF. Ista napetost oblikuje hibridne fajlove, koji postoje upravo da bi stariji čitači nastavili da rade uz tabele zasnovane na tokovima, kako je pokriveno u članku o hibridnim cross-reference tokovima u Office-generisanim PDF-ovima

Postoji i podni prag verzije. Linearizacija zahteva PDF 1.2 ili noviji. Ako je izabrana verzija starija, HotPDF je automatski podiže, osim ako je StrictVersionLock postavljen, u kom slučaju pisanje izaziva izuzetak umesto tihog unapređenja dokumenta koji ste namerno zaključali

Zid od 4 GiB, i zašto HotPDF odbija umesto da skraćuje

Hint tabele linearizacije čuvaju offsete kao 32-bitne vrednosti, tako da linearizovan fajl ne može adresirati ništa na ili preko 4 GiB, a HotPDF odbacuje takav izlaz sa eksplicitnim izuzetkom umesto da napiše fajl sa preklopljenim offsetima. Ograničenje nije izbor implementacije HotPDF-a; to je širina polja koje Aneks F definiše

Provera se primenjuje na tri mesta, i sva tri su bitna. HotPDF validira svaki objekat kad je njegova serijalizovana dužina poznata, validira dužinu svakog odeljka stranice dok gradi hint unose, i validira konačnu dužinu fajla nakon što je glavna cross-reference tabela dimenzionisana. Rani neuspeh je čitava poenta: hint tabela sa tiho skraćenim offsetom proizvodi fajl koji se ispravno otvara u čitaču koji ga preuzima ceo, a otkazuje samo kod klijenta sa opsegom bajtova za koji je linearizacija i postojala da posluži, što je najgori mogući režim otkaza jer vaš test čitač nikad to ne reprodukuje. Ako proizvodite izlaz od više gigabajta, linearizacija nije alat, i pristup strimovanja opisan u belešci o Direct File API za tokove rada sa velikim PDF-ovima je pravac za pogledati

Detekcija linearizacije na fajlu koji ste učitali

THotPDF.IsLoadedLinearized prijavljuje da li je trenutno učitani dokument već napisan u linearizovanom obliku, i odgovara iz snimka uzetog pre parsiranja, ne iz živog toka. HotPDF čita prvih 1024 bajta od pozicije nula izvornog toka, skenira ih tražeći prvu ključnu reč obj, a zatim unos /Linearized sa vrednošću 1, i keš-uje boolean rezultat

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;

Dva ograničenja u tom opisu su nosiva. Detekcija se ne može osloniti na poziciju toka, jer je do trenutka kad kod aplikacije postavi pitanje parser je već pomerio, i ne može ponovo čitati na zahtev jer LoadFromFile oslobađa interni izvorni tok kad se učitavanje završi. Otuda dizajn hvatanja-pre-parsiranja-i-keširanja. Skeniranje je takođe namerno doslovno o vrednosti: prihvata se samo /Linearized 1 ili numerički ekvivalentan oblik sa potpuno nultim razlomkom, jer fajl čiji rečnik parametara kaže nešto drugo ne daje obećanje Aneksa F

Delphi zamka sa zapisima vredna krađe

Lokalni zapisi koji sadrže dinamičke nizove inicijalizuju svoja upravljana polja i ništa drugo, i ako držite obično polje Count pored niza, morate ga sami očistiti. Ovo je ujelo podelu linearizacije tokom razvoja, i to je vrsta baga koja košta dan tačno zato što jedna platforma to sakriva

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

Polje dinamičkog niza je referentno brojano, tako da ga kompajler nulira. Count pored njega je obično celobrojno polje bez takve garancije, a neinicijalizovan Count šalje sam prvi append na proizvoljan indeks. Pod Win32 slot na steku je slučajno sadržao nulu, append je sleteo na indeks 0, i svaki test je prošao. Pod Win64 isti kod je pisao van kraja niza. Lekcija se generalizuje daleko van linearizacije: kad zapis meša upravljana i neupravljana polja, dodelite Default(TRecord) i prestanite da razmišljate koja polja kompajler pokriva, i nikad ne tretirajte zeleno Win32 pokretanje kao dokaz da je inicijalizacija ispravna

Članovi LinearizeOutput i IsLoadedLinearized opisani ovde isporučuju se sa standardnim HotPDF Component za Delphi i C++Builder; stranica proizvoda nosi kompletnu referencu svojstava, uključujući pravila interakcije sa cross-reference tokovima, tokovima objekata i zaključavanjem verzije