A PDFlibPas, a Delphihez és C++Builderhez készült natív VCL PDF-komponens könyvtár, egy oldal tartalomfolyamát a TPDFContentStateTracker osztályán keresztül játssza vissza, anélkül hogy egyáltalán hozzányúlna egy renderelő vászonhoz. Egyszerre egy elemzett operátort etetve a nyomkövetőbe egy futó grafikai-állapot-rekordot tart, elérhetővé téve pillanatképként bármely operátor végrehajtása előtt vagy után — az aktuális transzformációs mátrixot, a szövegmátrixot, a vágási határokat, és a q/Q mentés-vermet
Kérdezd meg, hol landol ténylegesen egy szövegfutam a nyomtatott oldalon, és a tartalomfolyam nyers számai önmagukban mindig félrevezetnek. A TPDFContentProgram.GetTextRuns már jelenti minden szövegmegjelenítő utasítás horgonypontját a TPDFTextRun OriginX és OriginY mezőin keresztül, és a mezőmegjegyzések egyértelműek abban, hogy ez a pont szövegtérben ül, már átvezetve a Tm, Td, TD, és T*-on. Ami még hiányzik, és amit azok a megjegyzések szerint egy hívónak kell szolgáltatnia, az a CTM, amely aktív abban a konkrét utasításban — minden eddig konkatenált cm szorzata, ágyazva bármennyi q/Q pár nyitva van éppen abban a pontban a folyamban
Miért érdemes egy tartalomfolyamot visszajátszani ahelyett hogy renderelnénk?
A PDFlibPas két különálló grafikai-állapot-fogalmat tart két különálló feladathoz, és a felosztás szándékos. A renderelő belső állapotrekordja egy élő eszköz-vászon-handle-t, egy vágási-régió-handle-t, és betűtípus-raszterizálási gyorsítótárakat hordoz — valódi erőforrásokat, amelyek bármilyen felülethez kötődnek, amelyet éppen festenek, és jelentés nélkülieket, amint az a felület eltűnik. A TPDFContentGraphicsState semmi ilyet nem hordoz: egy egyszerű rekord, amely azokra az értékekre korlátozódik, amelyeket az ISO 32000-1 §8.4 kizárólag tartalomfolyam-operátorokból elérhetőként definiál — a CTM, vonalstílus, szín, szövegállapot, és a levezetett vágás- és útvonalhatárok. Mivel a rekord nem hordoz vászon-hivatkozást és nyitott fájl-handle-t, egy hívó elemezhet egy tartalomfolyamot, bejárhatja a TPDFContentStateTrackerrel, és folytathatja a keletkező pillanatképek használatát jóval azután, hogy bármi, ami a bájtokat előállította, eltűnt
Hogyan építi fel a TPDFContentStateTracker a CTM-et
A TPDFContentStateTracker.Apply egy cm operátor hat operandusát konkatenálja a nyomkövető CTM-jébe ugyanazzal a premultiply-vel, amit maga a PDF meghatároz: az új mátrix M2 M2 × CTM-ként kombinálódik az aktuális CTM-mel, a sorvektor-konvencióban, ahol egy pont P′ = P × M-ként transzformálódik (ISO 32000-1 §8.4). A rész, amit könnyű elrontani, az eltolási tagban ül, nem a lineáris részben: M2 saját eltolásának át kell mennie az aktuális CTM forgatás-és-skálázás komponensén, mielőtt az aktuális CTM eltolása hozzáadódik a tetejére. Hagyd ki ezt a lépést, és kódold be helyette egy naiv, komponensenkénti kombinációt, és az első izolált cm, amit tesztelsz, helyesnek fog látszani, míg minden koordináta egy második vagy harmadik ágyazott cm alatt csendben elsodródik, ami pontosan az a fajta hiba, amely túléli a kódáttekintést, mert az egységteszt, amely elkapná, legalább két láncolt transzformációt igényel a buktatáshoz
var
Prog: TPDFContentProgram;
Runs: TPDFTextRunArray;
States: TPDFContentGraphicsStateArray;
DeviceX, DeviceY: Double;
I: Integer;
begin
Prog := TPDFContentProgram.Create;
try
Prog.Parse(ContentBytes);
Runs := Prog.GetTextRuns;
// One before-instruction snapshot per operator, computed in a single pass
States := Prog.TraceGraphicsStates(nil, False);
for I := 0 to High(Runs) do
begin
// OriginX/OriginY already fold in Tm/Td/TD/T*; only the CTM active
// at this instruction is still missing (ISO 32000-1 8.4)
with States[Runs[I].InstructionIndex].CTM do
begin
DeviceX := Runs[I].OriginX * M11 + Runs[I].OriginY * M21 + DX;
DeviceY := Runs[I].OriginX * M12 + Runs[I].OriginY * M22 + DY;
end;
LogTextOrigin(Runs[I].Text, DeviceX, DeviceY); // caller-supplied handler
end;
finally
Prog.Free;
end;
end;
A fenti ciklus megválaszolja a nyitányból ismert fájdalmas pontot: a TPDFContentProgram.GetTextRuns visszaadja az OriginX-et és OriginY-t, már átvezetve a Tm, Td, TD, és T*-on, és a TraceGraphicsStates(nil, False) szolgáltatja a maradék egy darabot, az utasítás-előtti CTM-et pontosan azon az indexen, ahol minden futamot rögzítettek, egyetlen lineáris átfutásban a teljes programon. A nil átadása lehetővé teszi, hogy a metódus birtokoljon egy privát nyomkövetőt a híváshoz, és belsőleg felszabadítsa azt, ami a helyes választás egy egyszeri szkenneléshez; egy meglévő TPDFContentStateTracker-példány átadása ehelyett az, ami folytonossá teszi az állapotot egy több tartalomfolyamból összeállított oldalon át, mivel az ISO 32000-1 egy oldal /Contents tömbjét egyetlen logikai folyamként kezeli, és a q/Q veremnek egyet kell értenie
A szövegmátrix túléli a Q-t; a grafikai állapot nem
Az ISO 32000-1 §9.4.2 definiálja a Td, TD, Tm, és T*-ot mint azokat az operátorokat, amelyek felépítik a szövegmátrixot és a szövegsor-mátrixot egy BT/ET blokkon belül, és a PDFlibPas élesen tartja ezt a megkülönböztetést: a Td és a TD egy tiszta eltolást konkatenál a szövegsor-mátrixra, a T* ugyanezt teszi az aktuális leading negatívjával, és csak a Tm cseréli le mindkét mátrixot egyenesen a neki adott hat számmal. A BT mindkét mátrixot visszaállítja az identitásra, pontosan egyszer, a szövegobjektum kezdetén — de a q és a Q egyáltalán nem nyúl hozzájuk. A TPDFContentStateTracker.Apply speciális esetként kezeli a coRestoreState-et pontosan emiatt: mielőtt lehúzná a mentett állapotot a veremről, rögzíti az aktuális szövegmátrixot, szövegsor-mátrixot, és BT/ET jelzőt, majd újra alkalmazza azokat bármi fölé, amit a lehúzott állapot tartott, mert egy szövegfutam köré csomagolt q/Q pár nem hivatott visszamozgatni a szövegpozíciót
var
Tracker: TPDFContentStateTracker;
Prog: TPDFContentProgram;
I: Integer;
begin
Prog := TPDFContentProgram.Create;
Tracker := TPDFContentStateTracker.Create;
try
Prog.Parse('BT 100 700 Td q 2 0 0 2 0 0 cm (A) Tj Q (B) Tj ET');
for I := 0 to Prog.Count - 1 do
begin
Tracker.Apply(Prog[I]);
if Prog[I].Op in [coShowText, coRestoreState] then
LogState(Prog[I].OpName, Tracker.Snapshot); // caller-supplied handler
end;
finally
Tracker.Free;
Prog.Free;
end;
end;
Futtasd ezt a sorozatot, és a második Tj-nél jelentett CTM visszaáll arra az identitás-skálára, ami q előtt volt — a 2 0 0 2 0 0 cm a mentés/visszaállítás páron belül eltűnt, ahogyan a q/Q megköveteli. A TextMatrix.DX ugyanabban az utasításban azonban még mindig 100: a Td, amely beállította, a q előtt futott, így nem olyan grafikai állapot, amihez a Q valaha is jogosult lett volna hozzányúlni, és egy eszköz, amely mást feltételezett volna, a második glyph-futamot az oldal rossz vízszintes pozíciójából indulóként jelentette volna
Mi történik, amikor egy vágásiútvonal-operátor fut?
Egy W vagy W* operátor nem zsugorítja azonnal a vágást; csak feljegyzi, melyik kitöltési szabályt kell használni, és a tényleges metszés arra az útvonal-festő operátorra vár, amelyik utána következik, beleértve a no-op festőt, az n-et, amit PDF-szerzők rutinszerűen épp arra használnak, hogy vágjanak anélkül hogy bármit rajzolnának. A TPDFContentStateTracker pontosan tükrözi ezt a kétlépéses időzítést: a coClip és a coClipEvenOdd csak beállít egy függőben lévő vágás-szabály jelzőt, és az EndCurrentPath — minden útvonal-festő operátor által meghívva — az, ami ténylegesen belemetszi a függőben lévő útvonal határait a ClipMinX, ClipMinY, ClipMaxX, és ClipMaxY-ba. Ennek a stádiumnak a helyes elvégzése magának az előtte/utána-pillanatkép szerződésnek számít: egy előtte-pillanatkép, amit pontosan a W utasításnál vesznek, még mindig a régi, szélesebb vágást kell mutassa, mert a vágás még nem lépett érvénybe azon a ponton a folyamban, és a két lépés eggyé olvasztása csendben eltörné minden hívót, amely arra támaszkodik, hogy az előtte-állapot azt jelentse, amit mond
A ClipBoundsExact megmondja egy hívónak, két helyzet közül melyiket nézi, és csak akkor True, ha egyetlen tengelyre igazított téglalapról van szó, amit az re épített egy egyébként üres útvonalon — az egyetlen alakzat, amit a PDFlibPas pontosan négy számként tud reprezentálni. Minden más — egy elforgatott téglalap, egy görbe körvonal, egy összetett útvonal több alútvonallal, vagy egy vágás, amely egy szövegrenderelési módból épült — még mindig előállítja a ClipMinX-től ClipMaxY-ig terjedőt, de a ClipBoundsExact False-ra törölve, egy őszinte jelzés, hogy a négy szám egy biztonságos külső határ, nem az igazi vágásalak; hívók, akiknek csak arra a határra van szükségük, mint egy téglalap alakú al-régió elszigetelése a GDI féltónusos lefelé-konverzió előtt, amit a PDF-oldalak 1 bites monokróm renderelése tárgyal, közvetlenül olvashatják azt, ahelyett hogy újralevezetnék az oldalgeometriából
Bézier-görbék: egy pontos vagy egy biztonságos határ
A legolcsóbb mód egy köbös Bézier-szegmens határolására a négy vezérlőpontjának konvex burkát venni, és ez mindig biztonságos, mert a görbe soha nem lép ki belőle — de egy sekély, széles görbe sokkal nagyobb határolódobozt jelenthet, mint amit a görbe ténylegesen elfoglal, ami gyengíti a vágás-alapú szűrést pontosan akkor, amikor a legjobban számít, nagy díszítő útvonalakon. A PDFlibPas a szorosabb problémát oldja meg ehelyett: minden tengelyhez megoldja a köbös görbe deriváltját a (0, 1) nyílt intervallumon belüli gyökökre, és kiértékeli a görbét bármely talált gyöknél, a két végponttal együtt, ami a szabványos zárt alakú módja annak, hogy egy görbe valódi tengelyre igazított kiterjedését kapjuk meg, nem egy túlbecslést. A görbénkénti pontosság azonban nem viszi át magára a vágásra: amint egy görbe körvonal vágási útvonallá válik, a ClipBoundsExact még mindig False-ra esik hozzá, mert egy határolódoboz, bármilyen szoros is, még mindig nem ugyanaz az alak, mint a görbe, amit határol, és az állapot-nyomkövető inkább kimondja ezt, minthogy hagyja, hogy egy hívó egy téglalapot feltételezzen ott, ahol ténylegesen egy görbe van
Állapot olvasása minden operátor előtt és után
Az, hogy egy hívó az előtte vagy utána állapotot akarja-e, teljesen attól függ, mit csinál az operátor: egy rajzolási vagy hit-tesztelési kérdés egy útvonalról vagy szövegfutamról az állapotot akarja, amilyen abban a pillanatban állt, közvetlenül azelőtt hogy az operátor futott, mivel az határozta meg ténylegesen, hogyan festette az operátor, míg egy diagnosztikai kérdés egy állapot-beállító operátorról, mint a gs, általában azt akarja látni, amit az épp megváltoztatott. A TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) pontosan ezt a választást teszi elérhetővé egyetlen logikai értékként, egy TPDFContentGraphicsState-et számítva ki utasításonként, egyetlen lineáris átfutásban a teljes programon, függetlenül attól, melyik pillanatot kérik. A GetGraphicsState(InstructionIndex, AfterInstruction, State) ugyanazt az előtte/utána választást kínálja egyetlen utasításhoz, nem a teljes programhoz, de minden híváskor a nulladik utasítástól játssza vissza, hogy odáig jusson, így sok index szkennelése egy ciklusban való meghívásával O(n²)-be kerül egy ugyanazon programon átvezető egyetlen O(n) hívással a TraceGraphicsStates-hez szemben
var
Before, After: TPDFContentGraphicsState;
begin
// Same instruction index, two different instants: before vs. after it runs
Prog.GetGraphicsState(CmIndex, False, Before);
Prog.GetGraphicsState(CmIndex, True, After);
// Before.CTM reflects every earlier cm; After.CTM already folds in
// this instruction's own concatenation as well
end;
Együttélés hibás formátumú tartalomfolyamokkal
Kétfajta hibás formátumú bemenet elég gyakori valódi PDF-előállítóknál ahhoz, hogy a TPDFContentStateTrackernek tolerálnia kelljen őket, ahelyett hogy elbukna rajtuk. Az első egy útvonal, amely átível egy q/Q határon: az aktuális útvonal, aktuális pont, és alútvonal-szám nem grafikaiállapot-paraméterek — az ISO 32000-1 §8.4 tárgyalja, mit ment és állít vissza a q és a Q, és az épülő aktuális útvonal nincs köztük —, így a TPDFContentStateTracker teljesen a mentett állapoton kívül követi ezt az adatot, és egy alútvonal, amely egy q előtt kezdődött, még mindig ott van, festetlenül, közvetlenül a megfelelő Q után. A második egy magányos Q, amelynek nincs hozzá tartozó q sehol előtte a folyamban, nem ritka a generátorok kimenetéből, amelyek tartalomfolyam-töredékeket fűznek össze, és elrontják a könyvelést. A TPDFContentStateTracker.RestoreUnderflowCount minden ilyen eseményt megszámol, ahelyett hogy kivételt dobna vagy sérült állapotot okozna: egy párosítatlan Q egyszerűen pontosan úgy hagyja az aktuális grafikai állapotot, ahogyan volt, mintha az az utasítás no-op lett volna, így a folyam többi része tovább játszódik egy épeszű állapoton, és egy hívó utólag még mindig eldöntheti, a számból, érdemes-e visszajelezni a bemenetet annak, aki előállította
A CTM-összeállítás, a szövegmátrix függetlensége a q/Q-tól, és egy vágási útvonal szakaszos megvalósítása nem függ attól, hogyan vagy hogy egyáltalán megfestik-e valaha a tartalomfolyamot, ami pontosan a lényeg: ugyanaz a TPDFContentStateTracker pillanatkép helyes, akár soha nem renderelik az oldalt, akár épp átadásra vár annak a háttérmotornak, amelyet a PDFlibPas kiválaszt ahhoz a fájlhoz, beleértve a futásidejű motorváltást, amit a PDFlibPas többmotoros PDF-rendereléséről szóló útmutató tárgyal. A tartalomelemzés, koordináta-leképezés, és kitakarás-eszközök mind teljesen futhatnak a nyomkövető kimenetén, jóval azelőtt, vagy teljesen anélkül, hogy valaha megkérnének egy renderelőt a bevonásra
A tartalomfolyam-visszajátszás a TPDFContentStateTrackeren keresztül a Delphihez és C++Builderhez készült natív VCL PDF-komponens könyvtárba, a PDFlibPasba épített strukturáltrartalom-szerkesztési keretrendszer része