PDFlibPas, natívna VCL knižnica komponentov PDF pre Delphi a C++Builder, prehráva obsahový stream strany prostredníctvom svojej triedy TPDFContentStateTracker bez toho, aby sa vôbec dotkla vykresľovacieho plátna. Odovzdávanie jedného spracovaného operátora po druhom do tohto sledovača udržiava priebežný záznam stavu grafiky – aktuálnu transformačnú maticu, textovú maticu, hranice orezania a zásobník ukladania q/Q – dostupný na zachytenie pred vykonaním alebo po vykonaní každého operátora
Skúste zistiť, kde na vytlačenej strane skutočne skončí konkrétny úsek textu, a samotné surové čísla z obsahového streamu vás vždy zavedú na nesprávnu stopu. TPDFContentProgram.GetTextRuns už hlási kotviaci bod každej inštrukcie zobrazujúcej text prostredníctvom polí OriginX a OriginY v TPDFTextRun a komentáre k poliam výslovne uvádzajú, že tento bod leží v textovom priestore, už prepočítaný cez Tm, Td, TD a T*. Čo stále chýba, a čo tieto komentáre hovoria, že si volajúci musí doplniť sám, je CTM aktívna presne pri danej inštrukcii – súčin každého cm zreťazeného doteraz, zanoreného v ľubovoľnom počte otvorených dvojíc q/Q v danom mieste streamu
Prečo prehrávať obsahový stream namiesto jeho vykreslenia?
PDFlibPas udržiava dva oddelené pojmy stavu grafiky pre dve oddelené úlohy, a toto rozdelenie je zámerné. Interný záznam stavu vykresľovača nesie živý handle plátna zariadenia, handle orezávacej oblasti a vyrovnávacie pamäte rasterizácie písma – skutočné zdroje viazané na akýkoľvek povrch, ktorý sa práve maľuje, a bezvýznamné, akonáhle tento povrch zanikne. TPDFContentGraphicsState nič z toho nenesie: je to obyčajný záznam obmedzený na hodnoty, ktoré ISO 32000-1 §8.4 definuje ako dosiahnuteľné výlučne z operátorov obsahového streamu – CTM, štýl čiary, farbu, textový stav a odvodené hranice orezania a cesty. Keďže tento záznam neobsahuje žiadnu referenciu na plátno ani otvorený súborový handle, volajúci môže spracovať obsahový stream, prejsť ho pomocou TPDFContentStateTracker a naďalej používať výsledné zachytené stavy dlho po tom, čo zanikne to, čo tieto bajty vôbec vytvorilo
Ako TPDFContentStateTracker zostavuje CTM
TPDFContentStateTracker.Apply zreťazuje šesť operandov operátora cm do CTM sledovača rovnakým predmnožením, aké špecifikuje samotné PDF: nová matica M2 sa kombinuje s aktuálnou CTM ako M2 × CTM, v konvencii riadkových vektorov, kde sa bod transformuje ako P′ = P × M (ISO 32000-1 §8.4). Časť, ktorú je ľahké pokaziť, sedí v translačnom člene, nie v lineárnej časti: vlastná translácia M2 musí prejsť cez rotačno-škálovaciu zložku aktuálnej CTM skôr, než sa navrch pripočíta translácia aktuálnej CTM. Vynechajte tento krok a namiesto toho natvrdo zakódujte naivnú kombináciu po zložkách, a prvý izolovaný cm, ktorý otestujete, bude vyzerať korektne, zatiaľ čo každá súradnica za druhým alebo tretím vnoreným cm bude potichu odchádzať od správnej hodnoty – presne taký druh chyby, ktorý prežije code review, pretože jednotkový test, ktorý by ju odhalil, potrebuje na zlyhanie aspoň dve zreťazené transformácie
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;
Cyklus vyššie zodpovedá bolestivé miesto z úvodu: TPDFContentProgram.GetTextRuns vracia OriginX a OriginY už prepočítané cez Tm, Td, TD a T*, a TraceGraphicsStates(nil, False) dodá ten posledný chýbajúci diel, CTM pred vykonaním presne na indexe, na ktorom bol daný úsek textu zachytený, v jedinom lineárnom priechode cez celý program. Odovzdanie nil necháva metódu vlastniť si súkromný sledovač pre dané volanie a interne ho uvoľniť, čo je správna voľba pre jednorazové skenovanie; odovzdanie existujúcej inštancie TPDFContentStateTracker naopak udrží stav priebežný naprieč stranou zloženou z viac než jedného obsahového streamu, keďže ISO 32000-1 pole /Contents strany považuje za jeden logický stream a zásobník q/Q sa musí zhodovať
Textová matica prežije Q; stav grafiky nie
ISO 32000-1 §9.4.2 definuje Td, TD, Tm a T* ako operátory, ktoré vnútri bloku BT/ET zostavujú textovú maticu a maticu textového riadka, a PDFlibPas toto rozlíšenie zachováva ostro: Td a TD zreťazujú čistú transláciu na maticu textového riadka, T* robí to isté pomocou zápornej hodnoty aktuálneho vedenia riadkov a jedine Tm rovno nahradí obe matice šiestimi číslami, ktoré dostane. BT resetuje obe matice na jednotkovú maticu, presne raz, na začiatku textového objektu – ale q a Q sa ich vôbec nedotýkajú. TPDFContentStateTracker.Apply špeciálne ošetruje coRestoreState presne z tohto dôvodu: skôr než vyberie uložený stav zo zásobníka, zachytí aktuálnu textovú maticu, maticu textového riadka a príznak BT/ET a znovu ich aplikuje nad tým, čo obsahoval vybratý stav, pretože dvojica q/Q obalená okolo úseku textu nemá za úlohu presunúť pozíciu textu späť
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;
Spustite túto postupnosť a CTM hlásená pri druhom Tj je späť na jednotkovej mierke, akú mala pred q – 2 0 0 2 0 0 cm vnútri dvojice ukladania a obnovenia je preč, presne ako to q/Q vyžaduje. TextMatrix.DX pri tej istej inštrukcii je však stále 100: Td, ktorý ju nastavil, prebehol pred q, takže nejde o stav grafiky, ktorého by sa Q vôbec smelo dotknúť, a nástroj, ktorý by predpokladal opak, by hlásil, že druhý úsek glyfov začína na nesprávnej vodorovnej pozícii na strane
Čo sa stane, keď prebehne operátor orezávacej cesty?
Operátor W alebo W* orezanie okamžite nezmenší; iba zaznamená, aké pravidlo výplne sa má použiť, a samotné pretínanie počká na akýkoľvek nasledujúci operátor kreslenia cesty, vrátane no-op operátora n, ktorý autori PDF bežne používajú presne na to, aby orezali bez toho, aby čokoľvek nakreslili. TPDFContentStateTracker toto dvojkrokové načasovanie presne odzrkadľuje: coClip a coClipEvenOdd iba nastavia príznak čakajúceho pravidla orezania, a EndCurrentPath – volaná každým operátorom kreslenia cesty – je tá, ktorá skutočne prietne hranice čakajúcej cesty do ClipMinX, ClipMinY, ClipMaxX a ClipMaxY. Správne odstupňovanie tohto procesu je dôležité aj pre samotnú zmluvu zachytenia pred/po: zachytenie stavu pred, urobené presne na inštrukcii W, musí stále ukazovať staré, širšie orezanie, pretože v tomto mieste streamu orezanie ešte nenadobudlo účinnosť, a zlúčenie oboch krokov do jedného by potichu pokazilo každého volajúceho, ktorý sa spolieha na to, že stav „pred“ znamená presne to, čo hovorí
ClipBoundsExact hovorí volajúcemu, s ktorou z dvoch situácií má do činenia, a je pravdivá len pre jediný osovo zarovnaný obdĺžnik vytvorený pomocou re nad inak prázdnou cestou – jediný tvar, ktorý dokáže PDFlibPas vyjadriť presne štyrmi číslami. Všetko ostatné – otočený obdĺžnik, zakrivený obrys, zložená cesta s viacerými podcestami alebo orezanie vytvorené z režimu vykresľovania textu – stále vyprodukuje ClipMinX až ClipMaxY, ale s ClipBoundsExact nastaveným na False, čo je čestný signál, že tieto štyri čísla sú bezpečnou vonkajšou hranicou, nie skutočným tvarom orezania; volajúci, ktorí potrebujú len túto hranicu, napríklad pri izolovaní obdĺžnikovej podoblasti pred GDI halftonovou konverziou opísanou v článku vykresľovanie strán PDF do 1-bitovej monochromatickej podoby, ju môžu čítať priamo namiesto toho, aby ju znovu odvodzovali z geometrie strany
Bézierove krivky: presná hranica alebo bezpečná hranica
Najlacnejší spôsob, ako ohraničiť segment kubickej Bézierovej krivky, je vziať konvexný obal jej štyroch riadiacich bodov, a to je vždy bezpečné, pretože krivka ho nikdy neopustí – no plytká, široká krivka môže vykázať ohraničujúci obdĺžnik oveľa väčší, než akú plochu krivka v skutočnosti zaberá, čo oslabuje filtrovanie založené na orezaní práve vtedy, keď na tom najviac záleží, pri veľkých dekoratívnych cestách. PDFlibPas namiesto toho rieši presnejší problém: pre každú os vyrieši deriváciu kubickej krivky pre korene vnútri otvoreného intervalu (0, 1) a vyhodnotí krivku v ľubovoľných nájdených koreňoch spolu s oboma koncovými bodmi, čo je štandardný uzavretý spôsob, ako získať skutočný osovo zarovnaný rozsah krivky namiesto nadhodnotenia. Presnosť na úrovni jednotlivej krivky sa však neprenáša na samotné orezanie: len čo sa zakrivený obrys stane orezávacou cestou, ClipBoundsExact preň aj tak klesne na False, pretože ohraničujúci obdĺžnik, akokoľvek tesný, stále nie je rovnaký tvar ako krivka, ktorú ohraničuje, a sledovač stavu radšej toto priznaje, než by nechal volajúceho predpokladať obdĺžnik tam, kde v skutočnosti je krivka
Čítanie stavu pred a po každom operátore
Či volajúci chce stav pred, alebo po, závisí úplne od toho, čo daný operátor robí: otázka o kreslení alebo hit-testovaní cesty či úseku textu chce stav taký, aký bol presne v okamihu pred vykonaním daného operátora, keďže práve ten určil, ako operátor vykreslil, zatiaľ čo diagnostická otázka o operátore nastavujúcom stav, ako je gs, zvyčajne chce vidieť, čo práve zmenil. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) vystavuje presne túto voľbu ako jedinú booleovskú hodnotu a vypočíta jeden TPDFContentGraphicsState na inštrukciu v jedinom lineárnom priechode cez celý program bez ohľadu na to, ktorý okamih sa požaduje. GetGraphicsState(InstructionIndex, AfterInstruction, State) ponúka rovnakú voľbu pred/po pre jedinú inštrukciu namiesto celého programu, ale pri každom volaní sa k nej dopracuje prehratím od nultej inštrukcie, takže skenovanie mnohých indexov volaním v cykle stojí O(n²) oproti jedinému volaniu TraceGraphicsStates s O(n) nad tým istým programom
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;
Práca s poškodenými obsahovými streamami
Dva druhy poškodeného vstupu sú v reálnych generátoroch PDF dostatočne bežné na to, že ich TPDFContentStateTracker musí tolerovať, nie na nich zlyhávať. Prvý je cesta, ktorá presahuje cez hranicu q/Q: aktuálna cesta, aktuálny bod a počet podciest nie sú parametre stavu grafiky – ISO 32000-1 §8.4 pokrýva to, čo q a Q ukladajú a obnovujú, a rozostavaná cesta medzi ne nepatrí – takže TPDFContentStateTracker sleduje tieto dáta úplne mimo uloženého stavu a podcesta začatá pred q je stále prítomná, nenakreslená, hneď po zodpovedajúcom Q. Druhý je osamotené Q bez zodpovedajúceho q kdekoľvek pred ním v streame, čo nie je zriedkavé u výstupov generátorov, ktoré skladajú fragmenty obsahového streamu zreťazením a pomýlia sa v účtovaní. TPDFContentStateTracker.RestoreUnderflowCount počíta každý takýto výskyt namiesto toho, aby vyvolal výnimku alebo poškodil stav: nespárované Q jednoducho ponechá aktuálny stav grafiky presne taký, aký bol, akoby táto inštrukcia bola no-op, takže zvyšok streamu sa naďalej prehráva na rozumnom stave a volajúci sa neskôr, na základe tohto počítadla, môže rozhodnúť, či stojí za to nahlásiť vstup späť tomu, kto ho vyprodukoval
Skladanie CTM, nezávislosť textovej matice od q/Q a odstupňovaná realizácia orezávacej cesty nezávisia od toho, ako alebo či sa obsahový stream vôbec niekedy vykreslí, a to je presne ten podstatný bod: rovnaké zachytenie z TPDFContentStateTracker je korektné bez ohľadu na to, či sa strana vôbec nikdy nevykreslí, alebo sa práve chystá odovzdať ktorémukoľvek backendu, ktorý si PDFlibPas pre daný súbor zvolí, vrátane prepínania enginu za behu opísaného v sprievodcovi viacenginovým vykresľovaním PDF v PDFlibPas. Analýza obsahu, mapovanie súradníc a nástroje na redakciu môžu bežať úplne nad výstupom sledovača, dávno predtým, alebo úplne bez toho, aby sa vôbec požiadal o zapojenie vykresľovač
Prehrávanie obsahového streamu prostredníctvom TPDFContentStateTracker je súčasťou štruktúrovaného rámca na úpravu obsahu zabudovaného do PDFlibPas, natívnej VCL knižnice komponentov PDF pre Delphi a C++Builder