A PDF-ek összefűzése elméletileg nem kellene, hogy erőforrás-igényes legyen. Az oldalak tartalma már el van rendezve, a betűtípusok be vannak ágyazva, a képek pedig tömörítve vannak. Elvben az összefűzés csupán adminisztráció: az objektumok újraszámozása, hogy a két fájl számozási tere ne ütközzön, az oldalfák összeillesztése, a kereszthivatkozási tábla javítása, majd az írás. A gyakorlatban azonban a legtöbb összefűző kód elpazarolja ezt a hatékonyságot. Minden egyes bemeneti fájl minden objektumát teljes mértékben értelmezi egy tokenizált objektumfává, módosít néhány indirekt hivatkozást, majd visszaszerializálja a fát bájtokká. Az értelmezés és az újraszerializálás a drága folyamatok, és az objektumok túlnyomó többségénél szinte pontosan ugyanolyan bájtsorozatot eredményeznek, mint ami bemenetként érkezett
A PDFlibPas egy natív Object Pascal PDF-motor Delphihez és C++Builderhez, és a gyors összefűzési útvonala azért létezik, hogy kihagyja ezt az oda-vissza utat minden olyan esetben, amikor az bizonyíthatóan biztonságos. Az elképzelés szűk körű, de teljes dokumentumkészletek esetén kifizetődik: egy nem módosított, nem stream típusú objektum esetében vegyük szó szerint az eredeti forrásbájtokat, és végezzünk el egyetlen bájtszintű átírást a bennük található indirekt hivatkozásokon, minden N G R hivatkozást (N+Offset) G R-re változtatva. Nincs tokenizáló, nincs objektumfa, nincs szerializáló. Ez a cikk bemutatja, hol jogszerű ez a rövidítés, az átírást végző értelmező állapotgép működését anélkül, hogy bármit is elrontana, miért igényelt a könyvjelzők összefűzése teljesen más mechanizmust, és hogyan épült át a normál összefűzési útvonal kvadratikusból lineárissá ezzel egy időben
Miért az objektumok újraszámozása az összefűzés valódi költsége
Minden PDF saját objektum-számozási térrel rendelkezik. Az A fájlban van 1. objektum, 2. objektum stb.; a B fájlnak is megvan a saját 1. objektuma, 2. objektuma és így tovább. Nem lehet a B objektumait változatlanul az A fájlba dobni, mert a számok ütköznének, and every indirect reference inside B would now resolve to the wrong object. A megoldás egy eltolás (offset): ha az A fájl az Offset objektumszámnál végződik, akkor a B fájl N-edik objektuma N+Offset objektummá válik a kimenetben, és a B objektumaiban megjelenő minden N G R hivatkozást (N+Offset) G R-re kell eltolni, hogy egyezzenek
Ez az eltolás jelenti a törzs összefűzésének teljes szemantikai feladatát. Az oldalfák javítása és az AcroForm összefűzése kisméretű, korlátozott módosítások néhány objektumon. A tömeges munka a hivatkozások átírása több ezer objektumon keresztül, és ennek naiv módja az egyes objektumok értelmezése, hogy szerkezetileg megtaláljuk a hivatkozásokat. A PDFlibPas MergeFileListFast eljárása az ellenkező megközelítést alkalmazza: a hivatkozások a nyers bájtokban is megtalálhatók, if you are careful about the contexts where a digit-space-digit-space-R sequence is not a reference. Ha kihagyjuk az értelmezést, és helyben végezzük el az eltolást, az objektumonkénti költség egyetlen lineáris vizsgálattá csökken azokon a bájtokon, amelyeket egyébként is másolnánk
Mikor bizonyíthatóan biztonságos a forrásbájtok újrafelhasználása
A bájtos útvonalat csak akkor választja a rendszer, ha a következő dokumentumból másolandó objektumra mindhárom feltétel teljesül. Bármelyikük hibája esetén az objektum visszakerül a teljes dekódolási és újraszerializálási útvonalra, így a helyesség mindig előbbre való a sebességnél:
Doc2.IsChangedObject(X)értéke False. Ha az összefűző motor már módosította az objektumot a memóriában (például egy oldaltárgyat, amelynek/Parenthivatkozását átirányították), a memóriában lévő fa a valóság forrása, és az eredeti bájtok elavultak. Csak a változatlan objektumok felelnek meg- A forrásbájtok nem tartalmaznak
streamkulcsszót. A stream objektum törzse egystream/endstreamközé zárt, átlátszatlan bináris adat, és a tömörített vagy titkosított stream adatokon végzett naiv hivatkozáskeresés boldogan „megtalálna” és elrontana olyan bájt-mintákat, amelyek hivatkozásnak tűnnek. A stream objektumok megtartják az eredeti, stream-tudatos útvonalat - A forrásbájtok nem tartalmaznak sem
/StructTreeRoot-ot, sem/StructElem-et. A gyors profilban a címkézett PDF struktúrafája (tagged-PDF structure tree) elvetésre kerül ahelyett, hogy összefűznénk, így ezeknek az objektumoknak a dekódolási útvonalon kell keresztülmenniük, ahol a motor szándékosan nullázhatja őket
A döntés az objektumonkénti másolási ciklusban dől el. Ha mindhárom ellenőrzés sikeres, az objektum bájtjai közvetlenül a ShiftIndRefsInSource eljáráshoz, majd az íróhoz (writer) kerülnek; ellenkező esetben a bájtokat elveti a rendszer, és az objektum a GetObject segítségével újra felépül, a ShiftIndRef-el eltolódik, majd szerializálódik. Ezen elágazás felépítését érdemes megnézni, mert az ellenőrzések sorrendje garantálja a biztonságot:
ObjectData := '';
if not Doc2.IsChangedObject(X) then
begin
ObjectData := FastMergeObjectSource(Reader2, X);
if (PLPos('stream', ObjectData) > 0) or
((not PreserveStructTree) and (PLPos('/StructTreeRoot', ObjectData) > 0)) or
((not PreserveStructTree) and (PLPos('/StructElem', ObjectData) > 0)) then
ObjectData := '' // fall back to decode
else
ObjectData := ShiftIndRefsInSource(ObjectData, Offset);
end;
if ObjectData <> '' then
Writer.AddObject(X + Offset, Doc2.GetGenNum(X), ObjectData)
else
begin
Obj := Doc2.GetObject(X, TempStruct); // full parse path
// ... null out struct-tree objects, ShiftIndRef, Obj.Output ...
end;
Az üres ObjectData jelzi, hogy a bájtos útvonal elutasította az objektumot. Ez az egyetlen őrszem (sentinel) megakadályozza a gyors és lassú útvonalak eltérését: pontosan egy helyen dől el a döntés, és pontosan egy biztonsági mentés létezik
A hivatkozás-eltoló állapotgép és annak határesetei
Az indirekt hivatkozások bájtszintű átírását megtévesztően könnyű elrontani, mivel az R karakter és a számsorozatok a PDF-objektumokban bárhol előfordulhatnak olyan kontextusban is, amely nem hivatkozás. A ShiftIndRefsInSource egy kisméretű, kézzel írt szkenner, amely egyszer járja be a bájtokat, és csak akkor ír át egy számot, ha azt a tokenek közötti PDF-szóközzel egy másik szám, majd egy R határoló követi. A gyors kilépések állnak az első helyen: hu az eltolás nulla vagy a forrás üres, a bájtok érintetlenül térnek vissza anélkül, hogy egyáltalán belépnének a szkennerbe
A szkenner helyessége azon múlik, hogy felismeri-e azokat a kontextusokat, ahol a hivatkozásnak látszó sorozatokat békén kell hagyni. Ezeket a határokat a legkönnyebb elszalasztani, és mindegyiket kifejezetten kezeli a rendszer:
- Literál karakterláncok (literal strings) amelyek
(és)jelekkel vannak határolva, szó szerint másolódnak, követve a beágyazási mélységet és tiszteletben tartva a visszaper (backslash) karaktert, így a levédett zárójel nem rontja el a mélységszámlálást. Egy olyan karakterlánc, mint a(see object 3 0 R for details), egy tipikus hivatkozás-mintát tartalmaz, ami valójában csak próza, és bájtról bájtra meg kell maradnia - Hexadecimális karakterláncok amelyek
<and>jelekkel vannak határolva, értelmezés nélkül kerülnek továbbításra. A hexadecimális karakterláncon belüli52bájtok azRASCII kódját jelentik, és egy olyan szkenner, amely a hexadecimális adatokat szövegként kezelné, fantom-hivatkozást hozhatna létre. A szótár megnyitó<<jele korábban észlelhető, így a szótár nem téveszthető össze egy hexadecimális karakterlánccal - Név-objektumok (name objects) amelyek
/jellel kezdődnek, teljes egészében feldolgozásra kerülnek, a perjeltől a következő szóközig vagy határolóig. Enélkül egy olyan név, mint a/R(egy gyakori erőforráskulcs), a hivatkozásRbetűjének minősülhetne - A
%jellel bevezetett megjegyzések a sor végéig tartanak, és átlátszatlan szövegként kimaradnak - A szám-majd-R teszt szigorú. A hivatkozás csak
NszóközGszóközRformátumban ismerhető fel, ahol azRkaraktert szóköz, határoló vagy a bemenet vége zárja le. Ha a generációs szám hiányzik, vagy azRkaraktert betű követi, a számjegyek változatlanul kerülnek kiírásra. Ez védi meg a/Length 1234egész számát és aMediaBoxnégy számát a csendes növeléstől
Ennek a szigorú tesztnek a magja szinte pontosan úgy olvasható, ahogyan a specifikáció leírja:
if (P <= N) and (Source[P] = 'R') and
((P = N) or PLIsPdfWhite(Source[P + 1]) or PLIsPdfDelimiter(Source[P + 1])) then
Obj1 := PLStrToIntDef(PLCopy(Source, I, E1 - I), -1);
if Obj1 >= 0 then
begin
AppendStr(PLIntToStr(Obj1 + Offset)); // eltolt objektumszám
AppendBytes(E1, P - E1); // eredeti szóköz + generáció
AppendBytes(P, 1); // az 'R'
end;
Csak az objektumszám kerül átírásra; a generációs szám és a tokenek közötti pontos eredeti szóköz átmásolódik, így a kimenet bájtra azonos a bemenetem, kivéve az egyetlen módosítandó egész számot. Ez a pontosság a lényeg — ez teszi a forrásbájtok újrafelhasználását egyenértékűvé a teljes újraszerializálással, nem pedig csak közelivé hozzá. Ezt a viselkedést egy célzott egységteszt-készlet fedi le, amely teszteli a tiszta hivatkozásokat, a tömbökön belüli hivatkozásokat, a nem-hivatkozási számokat, a literál karakterláncokat, a hexadecimális karakterláncokat és az eltolással ellátott nem-nulla generációs számokat
Miért nem tudták a könyvjelzők újrahasználni az AppendOutline eljárást
Több dokumentum könyvjelzőinek egyetlen vázfába (outline tree) történő összefűzése a meglévő AppendOutline segédfunkció feladatának tűnik, amely már tudja, hogyan kell egy dokumentum legfelső szintű könyvjelzőit egy másikhoz csatolni. Ez azonban nem a megfelelő eszköz ide, és ennek oka egy finom rétegződési eltérés. Az AppendOutline a meglévő utolsó felső szintű könyvjelzőt úgy határozza meg, hogy végigvezeti az olvasót (reader) az eredeti fájlbájtokon. A gyors összefűzés azonban a módosításait egy új objektumpufferbe helyezi a ChangeObject segítségével; az olvasó soha nem látja ezeket a szerkesztéseket. Három vagy több dokumentum láncolása esetén minden hozzáfűzés az első dokumentum eredeti utolsó könyvjelzőjét a legújabb dokumentumra irányítja át, így az összes köztes dokumentum könyvjelzője kiesik a láncból — csak a kumulatív /Count érték marad helyes, ami miatt a hiba könnyen elkerülheti a figyelmet, amíg valaki meg nem nyitja a könyvjelzőpanelt
Az egészet összekötő eltolási-igazítási invariáns
Mind a hivatkozás eltolása, mind a könyvjelző beillesztése egyetlen aritmetikai invariánstól függ, és ez a legérzékenyebb feltételezés a teljes tervezésben. A következő dokumentumba beillesztett hivatkozás a cél feltételezett globális objektumszáma mínusz a dokumentum Offsetje formában kerül beírásra, így amikor az objektum később a ShiftIndRef(Offset) segítségével eltolódik, az érték a kívánt globális számra esik. Az első dokumentum Offset = 0 értéket kap, és közvetlenül globális számokat használ. Ahhoz, hogy ez a kivonás helyes legyen, a beillesztés során használt futó eltolás-sorozatnak meg kell egyeznie az objektumok végső kiírásakor használt eltolás-sorozattal
Ez meg is egyezik az oldal- és űrlap-összefűzések működési tulajdonsága miatt: az AddPages, AddFields és AddFieldFonts csak az első dokumentum meglévő objektumait módosítja — soha nem ad hozzá újakat. Így az első dokumentum objektumszáma változatlan marad az oldalösszefűzési szakaszban, és minden következő dokumentum eltolása (az összes megelőző dokumentum objektumszámának összege) stabil marad a beillesztéstől a kiírásáig. Ha ezt megtörjük — például bevezetünk egy olyan szakaszt, amely új objektumot hoz létre az összefűzés közepén —, akkor minden oldalsó és könyvjelző hivatkozás eltolódna a hozzáadott objektumok számával. Az invariáns csendes, de alapvető jelentőségű
Három belépési pont egyetlen motor felett
A gyors útvonal nem az összefűző kód leágazása. Ugyanezen munka keretében a bájtszintű motor egyetlen belső rutinná alakult át: MergeFileListInternal(ListName, OutputFileName, PreserveStructTree, StrictMode), a nyilvános API-k pedig vékony burkolókká (wrappers) váltak, amelyek két jelzőt választanak ki:
- A
MergeFileListFasta struktúrafa megőrzése nélkül hívja meg a motort — ez a leggyorsabb útvonal, amely elveti a címkézett PDF fát, így a bájtos útvonal a legtöbb objektumra érvényes - A
MergeFileListmegőrzéssel hívja meg, így a struktúrafa megmarad, és az eredmény egy használható címkézett PDF lesz. Ez a normál útvonal örökli a több dokumentumra kiterjedő könyvjelző- és űrlap-összefűzést is - A
MergeFileListStrictbekapcsolja a szigorú módot: az első metaadat-bejárás megáll az első olyan bemenetnél, amely nem jelent tiszta összefűzést, így csak a hibás fájl előtt összegyűjtött dokumentumok kerülnek bele, ahelyett, hogy kihagyná a hibás fájlt és folytatná
Az útvonalak összevonása azt is lehetővé tette, hogy a normál összefűzést a páronkénti O(N²) ciklusból — az első és második fájl összefűzése, majd az eredmény összefűzése a harmadikkal stb., minden lépésben újraértelmezve a növő akkumulátort — egyetlen lineáris menetbe építsük át, amely minden bemenetet csak egyszer nyit meg. A két régóta meglévő kétfájlos és kétstreames belépési pont, a MergeFiles és a MergeStreams, érintetlen maradt, és továbbra is elérhető a páronkénti összefűzést igénylő hívók számára
Egy őszinte megjegyzés a struktúrafa viselkedésével kapcsolatban, mert ez megnehezítette a tesztcsomag dolgát. A gyors útvonal általi „elvetés” nem teljes: eltávolítja az első dokumentum katalógushivatkozását a /StructTreeRoot-ra, de maga a struktúrafa-objektum továbbra is kiíródik árvaként. Így a gyors kimenet bájtjai továbbra is tartalmazzák a /StructTreeRoot karakterláncot, és nem lehet megkülönböztetni a gyors és a normál kimenetet a karakterlánc keresésével — a valódi különbség az, hogy a katalógus eléri-e még a struktúrafát, ami meghatározza, hogy a fájl továbbra is navigálható címkézett PDF marad-e
Mikor melyik útvonalat válasszuk
A bájtos útvonal egy átviteli teljesítményre optimalizált megoldás sok olyan dokumentum összeállítására, ahol nincs szükség a címkézett PDF struktúrafájának megőrzésére — jelentések kötegelése, kimutatások futtatása, csoportos összefűzés. Közepes és nagy bemeneti készletek ismételt összefűzésével mérve a bájtok újrafelhasználása az objektumok összetételétől függően nagyjából 4-13 százalékkal csökkentette a valós időt, anélkül, hogy újabb hibák jelentkeztek volna kis vagy hibás bemenetek esetén, mivel minden olyan objektum, amelyet a szkenner nem tud biztonságosnak bizonyítani, visszatér a teljes értelmezéshez. Ha az akadálymentesítés érdekében sértetlen struktúrafára van szüksége, használja a normál tagged-PDF összefűzési útvonalat, amely megőrzi azt; ha pedig nagyon nagy méretű egyedi fájlokkal dolgozik sok bemenet helyett, a nagy PDF-fájlok közvetlen fájleléréssel történő összefűzéséről és felosztásáról szóló társoldalon leírt bájtmásolási technikák ugyanezt a „bájtok másolása, teljes objektumfa elkerülése” elvet alkalmazzák fájlméretben
Az összefűző eljárások, valamint azok gyors és szigorú változatai a PDFlibPas Delphi PDF Library részét képezik, amelynek dokumentációja tartalmazza a fájllista API és az itt leírt összefűzési lehetőségek teljes leírását