Műszaki cikk

Linearizált PDF-kimenet Delphiben: HotPDF hinttáblák

A HotPDF linearizált PDF-fájlokat ír, azt az elrendezést, amelyet az Acrobat Fast Web View néven jelöl, a THotPDF LinearizeOutput tulajdonságán keresztül. Ha ezt a BeginDoc előtt állítjuk be, a HotPDF átrendezi a kész objektumgráfot, így egy bájttartomány-tudatos olvasó meg tudja jeleníteni az első oldalt, miután csak a fájl kezdő részét lekérte, ahelyett hogy az egész dokumentumot előbb le kellene töltenie. A mechanizmus az ISO 32000-1 F. függeléke

Az, hogy ez miért számít, meglehetősen prózai. Egy normál PDF a keresztreferencia-táblát a végére teszi, így egy megjelenítőnek el kell érnie az utolsó bájtot, mielőtt tudná, hol van bármi. Adjunk egy böngészőnek egy 200 oldalas szkennelt jelentést, és a felhasználó a teljes átvitel alatt egy töltésjelzőt bámul, miközben csak az 1. oldalt szerette volna. A linearizáció ezt orvosolja azzal, hogy az árat íráskor fizeti meg. Ez a cikk kifejezetten erről az írási útvonalról szól: a partícionálásról, a mérési ciklusról és a szigorú korlátokról; a Fast Web View mögötti fogalmi hátteret a korábbi a PDF-linearizáció és a Fast Web View magyarázata cikk tárgyalja

Mit garantál valójában a linearizált elrendezés

Egy linearizált fájl egy közönséges PDF, rendkívül specifikus fizikai sorrenddel, és minden garancia, amelyet nyújt, ebből a sorrendből ered, nem valamiféle új objektumtípusból. A HotPDF az F. függelék által előírt sorrendben adja ki a részeket: a linearizációs paraméterszótár az első 1024 bájton belül, egy korai keresztreferencia-tábla, a dokumentumszintű objektumok, az elsődleges hintfolyam, az első oldal és annak privát objektumai, majd a fennmaradó oldalak, majd a megosztott objektumok, majd minden más, végül a fő keresztreferencia-tábla

A partícionálás levezetett, nem deklarált. A HotPDF minden egyes oldalobjektumból bejárja a hivatkozási gráfot, és minden közvetett objektumhoz rögzíti, hány oldal éri el, és melyik oldal érte el elsőként. Egy objektum, amelyet pontosan egy oldal használ, annak az oldalnak privátjává válik. Egy objektum, amelyet egynél több oldal ér el, megosztottá válik. A katalógus, plusz bármi, amire a /ViewerPreferences, /OpenAction, /Threads és /AcroForm alatt hivatkozik, plusz a titkosítási szótár, amikor a védelem aktív, alkotja a dokumentumszintű csoportot, amelynek meg kell előznie mindent. Az oldalfa-csomópontokat szándékosan visszatartják, hogy ne szennyezzék az első oldal szakaszát

A paraméterszótár azokat a számokat hordozza, amelyekre egy olvasónak szüksége van, mielőtt bármi mást olvasott volna: /L a teljes fájlhosszhoz, /H a hintfolyam eltolásához és hosszához, /O az első oldal objektumszámához, /E ahhoz a bájthoz, ahol az első oldal szakasza véget ér, /N az oldalszámhoz, /T pedig a fő keresztreferencia-tábla bejegyzésének eltolásához. Ezek mindegyike egy bájteltolás egy olyan fájlba, amely még nem is létezik abban a pillanatban, amikor le kell írnunk őket

Miért kell konvergálnia a hinttábla eltolásainak?

Mert a paraméterszótárban lévő számok azt a fájlt írják le, amely tartalmazza őket, és bármelyikük megváltoztatása megváltoztatja a fájlt. Ez a linearizált író központi nehézsége, és ezért méri a HotPDF ismételten ahelyett, hogy egyszer írna. Ha a /T-t 6 számjegyről 7-re szélesítjük, a paraméterszótár egy bájttal nő; a fejléc nő; minden objektum elmozdul; a fő keresztreferencia-tábla elmozdul; a /T-nek most más érték kell. Az elrendezésnek fixpontot kell elérnie, mielőtt a valós kimenet egyetlen bájtja is véglegesítésre kerül

A HotPDF ezt egy korlátozott iterációval kezeli. Először minden objektumot egy számláló folyamba szerializál, amely hosszt rögzít bájtok megtartása nélkül, így minden objektumnak ismert szerializált mérete van. Ezután lefuttat egy elrendezési lépést, amely eltolásokat rendel a dokumentumszintű csoporthoz, a hintfolyamhoz, az első oldal csoportjához, a későbbi oldalcsoportokhoz, a megosztott csoporthoz és a maradékhoz, és jelenti, hova kerülne a fő keresztreferencia-tábla. Ez az eredmény visszakerül bemenetként a következő lépéshez. A ciklus nyolc próbálkozásra van korlátozva, a nem konvergálás pedig kivételt dob ahelyett, hogy hihetőnek tűnő, de hibás eltolásokkal rendelkező fájlt állítana elő

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

Két részlet tartja vissza a ciklust a csapkodástól. A paraméterszótár egy fix, 384 bájtos, szóközökkel kitöltött helyre kerül írásra, így annak saját növekedése soha nem tudja destabilizálni az elrendezést; ha a szótár szövege valaha is meghaladná ezt a foglalást, a HotPDF kivételt dob, ahelyett hogy csendben eltolna mindent. A konvergencia után a HotPDF lefuttat még egy megerősítő elrendezési lépést, és újra ellenőrzi a hintfolyam hosszát, mert maga a hintfolyam is olyan eltolásokat kódol, amelyek csak akkor voltak ismertek, amikor az elrendezés megállapodott. Az összes mérés hozadéka, hogy a HotPDF soha nem puffereli a dokumentum második másolatát: az eltolások rögzítése után az objektumok közvetlenül a célfolyamba szerializálódnak, minden szakaszhatárnál egy assertion mellett, amely ellenőrzi, hogy a leírt bájtok egyeznek az ígért eltolással

Bekapcsolás Delphiből

Az API felülete egyetlen Boolean, és az egyetlen követelmény, hogy a generálás megkezdése előtt állítsuk be. A LinearizeOutput alapértelmezett értéke False, és az elrendezési lépés a dokumentum írásakor fut le, így ha az EndDoc után rendeljük hozzá, semmit nem ér el

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;

Egy telepítési figyelmeztetés mindenen felülír a kódoldalon. A linearizáció csak akkor térül meg, ha a szállítás támogatja a HTTP tartományalapú kéréseket. Ha ugyanazt a fájlt egy olyan végponton keresztül szolgáljuk ki, amely egészben streameli, vagy egy olyan CDN-konfigurációval, amely figyelmen kívül hagyja a Range fejlécet, akkor lassabb írási útvonalat és nagyobb fájlt vásároltunk magunknak, felhasználó számára látható nyereség nélkül. Ellenőrizzük a szervert, mielőtt a kódot ellenőriznénk

Miért írja felül a linearizáció a UseXRefStream és a UseObjectStreams beállítást?

Mert a linearizált írónak minden objektumnak saját, közvetlenül címezhető bájteltolással kell rendelkeznie, és mindkét funkció ezt veszi el. A HotPDF ezért hagyományos szöveges keresztreferencia-táblákat és kicsomagolt közvetett objektumokat ad ki, amikor a LinearizeOutput engedélyezve van, még akkor is, ha a hívó a UseXRefStream-et vagy a UseObjectStreams-et is beállította. Ez szándékos felülírás, nem olyan konfliktus, amelyet magunknak kellene feloldanunk

Az indoklás a hinttáblákból következik. Egy hinttábla leírja, hol kezdődik egy oldal szakasza, és milyen hosszú, így egy olvasó pontosan azt a tartományt tudja kérni. Egy /ObjStm konténerbe csomagolt objektumnak egyáltalán nincs önálló eltolása; csak egy szeletként létezik egy másik tömörített folyamban, amelyet egységként kell lekérni és kitömöríteni. Ha a fájlméret miatt számítottunk az objektumfolyamokra, tudnunk kell, hogy a linearizáció és a tömörítés itt ellentétes irányba húz, a kompromisszumot pedig a HotPDF objektumfolyamairól és inkrementális frissítéseiről szóló kísérőcikkben olvashatjuk. Ugyanez a feszültség alakítja a hibrid hivatkozású fájlokat is, amelyek pontosan azért léteznek, hogy a régebbi olvasók a folyamalapú táblák mellett is működjenek, ahogy azt az Office által generált PDF-ek hibrid keresztreferencia-folyamairól szóló cikk tárgyalja

Van egy verziós alsó korlát is. A linearizáció PDF 1.2-t vagy újabbat igényel. Ha a kiválasztott verzió régebbi, a HotPDF automatikusan felemeli, hacsak a StrictVersionLock nincs beállítva, ebben az esetben az írás kivételt dob ahelyett, hogy csendben feljebb léptetne egy dokumentumot, amelyet szándékosan rögzítettünk

A 4 GiB-os fal, és miért utasítja el a HotPDF a levágás helyett

A linearizációs hinttáblák az eltolásokat 32 bites értékként tárolják, így egy linearizált fájl nem tud semmit megcímezni 4 GiB-nál vagy azon túl, és a HotPDF egy explicit kivétellel utasítja el az ilyen kimenetet ahelyett, hogy körbeforduló eltolásokkal rendelkező fájlt írna. A korlát nem HotPDF-implementációs döntés; ez az F. függelék mezőinek szélessége

Az ellenőrzés három helyen kerül alkalmazásra, és mind a három számít. A HotPDF minden objektumot ellenőriz, amint annak szerializált hossza ismertté válik, ellenőrzi minden oldalszakasz hosszát a hintbejegyzések felépítésekor, és ellenőrzi a végleges fájlhosszt a fő keresztreferencia-tábla méretezése után. A korai bukás a lényeg: egy csendben levágott eltolással rendelkező hinttábla olyan fájlt eredményez, amely megfelelően nyílik meg egy azt egészben letöltő megjelenítőben, és csak annál a bájttartomány-alapú kliensnél hiúsul meg, amelynek kiszolgálására a linearizáció létezik, ez pedig a lehető legrosszabb hibamód, mert a teszt-megjelenítőnk soha nem reprodukálja azt. Ha többgigabájtos kimenetet állítunk elő, a linearizáció nem a megfelelő eszköz, és a nagy PDF-workflow-khoz szánt Direct File API-ról szóló jegyzetekben leírt streamelési megközelítés az irány, amerre nézni érdemes

Linearizáció felismerése egy betöltött fájlon

A THotPDF.IsLoadedLinearized jelenti, hogy az aktuálisan betöltött dokumentum már linearizált formában volt-e megírva, és egy elemzés előtt készített pillanatképből válaszol, nem az élő folyamból. A HotPDF beolvassa az első 1024 bájtot a forrásfolyam nulladik pozíciójáról, végigpásztázza az első obj kulcsszóért, majd egy /Linearized bejegyzésért 1-es értékkel, és gyorsítótárazza a boolean eredményt

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;

Két megkötés ebben a leírásban tartóoszlop-jellegű. A felismerés nem támaszkodhat a folyam pozíciójára, mert mire az alkalmazáskód feltenné a kérdést, az elemző már elmozdította azt, és nem tud igény szerint újraolvasni, mert a LoadFromFile a betöltés befejeztével felszabadítja a belső forrásfolyamot. Innen ered az elemzés előtti rögzítés és gyorsítótárazás terve. A pásztázás szándékosan szó szerinti az érték tekintetében is: csak a /Linearized 1 vagy egy vele numerikusan egyenértékű, teljesen nulla törtrészű forma kerül elfogadásra, mert egy olyan fájl, amelynek paraméterszótára mást mond, nem teszi meg az F. függelék ígéretét

Egy Delphi rekordcsapda, amelyet érdemes eltanulni

A dinamikus tömböket tartalmazó lokális rekordok csak a kezelt mezőiket inicializálják, semmi mást, és ha a tömb mellett egy egyszerű Count mezőt is tartunk, azt magunknak kell törölnünk. Ez a fejlesztés során megharapta a linearizációs partícionálást, és pontosan az a fajta hiba, amely egy napot vesz el, mert az egyik platform elrejti

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

A dinamikus tömb mező referenciaszámlált, így a fordító nullázza azt. A mellette lévő Count egy közönséges egész szám, semmilyen ilyen garancia nélkül, és egy inicializálatlan Count az első hozzáfűzést egy tetszőleges indexre küldi. Win32 alatt a veremhely történetesen nullát tartalmazott, a hozzáfűzés a 0. indexre került, és minden teszt sikeres volt. Win64 alatt ugyanaz a kód a tömb végén túlírt. A tanulság jóval túlmutat a linearizáción: amikor egy rekord kezelt és nem kezelt mezőket kever, rendeljük hozzá a Default(TRecord)-ot, és ne találgassunk tovább, mely mezőket fedi le a fordító, és soha ne kezeljünk egy zöld Win32-futást az inicializálás helyességének bizonyítékaként

Az itt leírt LinearizeOutput és IsLoadedLinearized tagok a standard HotPDF Component részeként érhetők el Delphihez és C++Builderhez; a termékoldal hordozza a teljes tulajdonságreferenciát, beleértve a keresztreferencia-folyamokkal, az objektumfolyamokkal és a verziózárolással való interakciós szabályokat