PDFlibPas, nativní knihovna komponenty PDF pro VCL pro Delphi a C++Builder, přehrává proud obsahu stránky přes svou třídu TPDFContentStateTracker, aniž by se vůbec dotkla vykreslovacího plátna. Podávání jednoho naparsovaného operátoru trackeru najednou udržuje běžící záznam stavu grafiky — aktuální transformační matici, textovou matici, hranice ořezu a zásobník ukládání q/Q — dostupný pro snímek před nebo po vykonání každého operátoru
Zeptejte se, kam skutečně na vytištěné stránce přistane běh textu, a surová čísla proudu obsahu samotná vás pokaždé zmatou. TPDFContentProgram.GetTextRuns už hlásí kotvící bod každé instrukce zobrazující text přes pole OriginX a OriginY na TPDFTextRun, a komentáře pole jsou explicitní v tom, že tento bod sedí v prostoru textu, už protažený přes Tm, Td, TD a T*. Co pořád chybí, a co komentáře říkají, že volající musí dodat, je CTM aktivní na této přesné instrukci — součin každého cm zřetězeného doposud, vnořený uvnitř toho, kolik párů q/Q je v tomto bodě proudu právě otevřených
Proč přehrávat proud obsahu místo jeho vykreslení?
PDFlibPas udržuje dva samostatné pojmy stavu grafiky pro dvě samostatné úlohy, a rozdělení je záměrné. Interní záznam stavu vykreslovače nese živý handle plátna zařízení, handle ořezové oblasti a cache rasterizace fontů — skutečné zdroje vázané na jakoukoli plochu, která se právě maluje, a bez významu, jakmile tato plocha zanikne. TPDFContentGraphicsState nic z toho nenese: je to obyčejný záznam omezený na hodnoty, které ISO 32000-1 §8.4 definuje jako dosažitelné jen z operátorů proudu obsahu — CTM, styl čáry, barvu, stav textu a odvozené hranice ořezu a cesty. Protože záznam nedrží žádný odkaz na plátno a žádný otevřený handle souboru, volající může naparsovat proud obsahu, projít jej přes TPDFContentStateTracker, a pokračovat v používání výsledných snímků dlouho poté, co cokoli, co vyprodukovalo bajty, zaniklo
Jak TPDFContentStateTracker staví CTM
TPDFContentStateTracker.Apply zřetězí šest operandů operátoru cm do CTM trackeru pomocí stejného předvynásobení, které specifikuje samotné PDF: nová matice M2 se kombinuje s aktuálním CTM jako M2 × CTM, v konvenci řádkových vektorů, kde se bod transformuje jako P′ = P × M (ISO 32000-1 §8.4). Část, kterou je snadné pokazit, sedí v translačním členu, ne v lineární části: vlastní translace M2 musí projít přes rotační a škálovací složku aktuálního CTM dřív, než se navrch přičte translace aktuálního CTM. Přeskočte tento krok a natvrdo zakódujte místo toho naivní kombinaci po složkách, a první izolovaný cm, který otestujete, bude vypadat správně, zatímco každá souřadnice po směru od druhého nebo třetího vnořeného cm tiše odplouvá, což je přesně ten druh chyby, který přežije code review, protože jednotkový test, který by ji odchytil, potřebuje selhat alespoň na dvou zřetězených transformacích
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;
Smyčka výše odpovídá na bolestivé místo z úvodu: TPDFContentProgram.GetTextRuns vrátí OriginX a OriginY už protažené přes Tm, Td, TD a T*, a TraceGraphicsStates(nil, False) dodá jediný chybějící kus, CTM před instrukcí přesně na tom indexu, na kterém byl každý běh zachycen, v jediném lineárním průchodu přes celý program. Předání nil nechá metodu vlastnit soukromý tracker pro toto volání a interně jej uvolnit, což je správná volba pro jednorázový sken; předání existující instance TPDFContentStateTracker místo toho je to, co udrží stav souvislý přes stránku sestavenou z více než jednoho proudu obsahu, protože ISO 32000-1 zachází s polem stránky /Contents jako s jedním logickým proudem a zásobník q/Q se s tím musí shodovat
Textová matice přežije Q; stav grafiky ne
ISO 32000-1 §9.4.2 definuje Td, TD, Tm a T* jako operátory, které staví textovou matici a matici textového řádku uvnitř bloku BT/ET, a PDFlibPas toto rozlišení drží ostré: Td a TD zřetězí čistou translaci na matici textového řádku, T* dělá totéž pomocí záporné hodnoty aktuálního odsazení, a jen Tm nahradí obě matice rovnou šesti čísly, která dostane. BT resetuje obě matice na jednotkovou, přesně jednou, na začátku textového objektu — ale q a Q se jich vůbec nedotýkají. TPDFContentStateTracker.Apply speciálně zpracovává coRestoreState přesně z tohoto důvodu: dřív, než vyskočí uložený stav ze zásobníku, zachytí aktuální textovou matici, matici textového řádku a příznak BT/ET, a znovu je aplikuje přes cokoli, co vyskočený stav náhodou nesl, protože se od páru q/Q obaleného kolem běhu textu neočekává, že by textovou pozici posunul zpátky
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;
Spusťte tuto sekvenci a CTM hlášené na druhém Tj je zpátky na jednotkovém měřítku, jaké mělo před q — 2 0 0 2 0 0 cm uvnitř páru save/restore je pryč, jak q/Q vyžaduje. TextMatrix.DX na téže instrukci je ale pořád 100: Td, které jej nastavilo, běželo před q, takže to není stav grafiky, kterého se Q kdy směl dotknout, a nástroj, který by předpokládal opak, by nahlásil druhý běh glyfů začínající na špatné vodorovné pozici na stránce
Co se stane, když se spustí operátor ořezové cesty?
Operátor W nebo W* okamžitě nezmenší ořez; jen zaznamená, které pravidlo výplně se má použít, a skutečný průnik čeká na jakýkoli operátor malování cesty, který jej následuje, včetně no-op malíře n, který autoři PDF rutinně používají přesně k ořezu bez čehokoli kreslení. TPDFContentStateTracker toto dvoukrokové časování přesně zrcadlí: coClip a coClipEvenOdd jen nastaví čekající příznak pravidla ořezu, a EndCurrentPath — vyvolávaný každým operátorem malování cesty — je to, co skutečně protne hranice čekající cesty do ClipMinX, ClipMinY, ClipMaxX a ClipMaxY. Správné načasování tohoto stagingu je důležité pro samotný kontrakt snímků před/po: snímek před vzatý přesně na instrukci W musí pořád ukazovat starý, širší ořez, protože ořez ještě v tomto bodě proudu nenabyl účinnosti, a sloučení obou kroků do jednoho by tiše rozbilo každého volajícího spoléhajícího na to, že stav-před znamená to, co říká
ClipBoundsExact říká volajícímu, na kterou ze dvou situací se dívá, a je True jen pro jediný osově zarovnaný obdélník postavený přes re na jinak prázdné cestě — jediný tvar, který PDFlibPas dokáže reprezentovat přesně jako čtyři čísla. Všechno ostatní — otočený obdélník, křivkový obrys, složená cesta s několika dílčími cestami, nebo ořez postavený z režimu vykreslování textu — pořád vyprodukuje ClipMinX až ClipMaxY, ale s ClipBoundsExact vyčištěným na False, poctivý signál, že těchto čtyři čísel je bezpečná vnější mez, ne skutečný tvar ořezu; volající, kteří potřebují jen tuto mez, jako izolování obdélníkové dílčí oblasti před down-konverzí halftonu GDI popsanou v vykreslování stránek PDF do 1bitové monochromatické podoby, ji mohou přečíst přímo místo toho, aby ji znovu odvozovali z geometrie stránky
Béziérovy křivky: přesná mez, nebo bezpečná
Nejlevnější způsob, jak ohraničit kubický segment Bézier, je vzít konvexní obal jeho čtyř řídicích bodů, a to je vždy bezpečné, protože křivka jej nikdy neopustí — ale mělká, široká křivka dokáže nahlásit ohraničující box mnohem větší, než křivka skutečně zabírá, což oslabí filtrování založené na ořezu přesně tehdy, když na tom nejvíc záleží, na velkých dekorativních cestách. PDFlibPas místo toho řeší těsnější problém: pro každou osu vyřeší derivaci kubické křivky na kořeny uvnitř otevřeného intervalu (0, 1) a vyhodnotí křivku na jakýchkoli nalezených kořenech, spolu s oběma koncovými body, což je standardní uzavřený způsob, jak získat skutečný osově zarovnaný rozsah křivky místo nadhodnocení. Přesnost po jednotlivých křivkách se ale nepřenáší na samotný ořez: jakmile se křivkový obrys stane ořezovou cestou, ClipBoundsExact pro něj pořád spadne na False, protože ohraničující box, byť sebetěsnější, pořád není stejný tvar jako křivka, kterou ohraničuje, a tracker stavu by to raději řekl, než nechal volajícího předpokládat obdélník tam, kde skutečně je křivka
Čtení stavu před a po každém operátoru
Zda volající chce stav před nebo po, úplně závisí na tom, co operátor dělá: kreslicí nebo hit-testovací otázka o cestě nebo běhu textu chce stav takový, jaký byl v okamžiku těsně před tím, než tento operátor běžel, protože to je to, co skutečně určilo, jak operátor maloval, zatímco diagnostická otázka o operátoru nastavujícím stav jako gs obvykle chce vidět, co právě změnil. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) vystavuje přesně tuto volbu jako jediný booleovský parametr a spočítá jeden TPDFContentGraphicsState na instrukci v jediném lineárním průchodu přes celý program bez ohledu na to, který okamžik je požadován. GetGraphicsState(InstructionIndex, AfterInstruction, State) nabízí stejnou volbu před/po pro jedinou instrukci místo celého programu, ale při každém volání přehrává od instrukce nula, aby se tam dostal, takže procházení mnoha indexů voláním ve smyčce stojí O(n²) proti jedinému O(n) volání TraceGraphicsStates přes stejný program
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;
Život s poškozenými proudy obsahu
Dva druhy poškozeného vstupu jsou u skutečných producentů PDF dost běžné na to, aby je TPDFContentStateTracker musel tolerovat místo toho, aby na nich selhal. První je cesta, která přesahuje hranici q/Q: aktuální cesta, aktuální bod a počet dílčích cest nejsou parametry stavu grafiky — ISO 32000-1 §8.4 popisuje, co q a Q ukládají a obnovují, a právě stavěná aktuální cesta mezi tím není — takže TPDFContentStateTracker sleduje tato data úplně mimo uložený stav, a dílčí cesta začatá před q je tam pořád, nevymalovaná, ihned po odpovídajícím Q. Druhý je holé Q bez odpovídajícího q kdekoli před ním v proudu, nevzácné ve výstupu z generátorů, které sestavují fragmenty proudu obsahu zřetězením a pokazí účetnictví. TPDFContentStateTracker.RestoreUnderflowCount počítá každou takovou událost místo toho, aby vyvolal výjimku nebo poškodil stav: nespárované Q prostě ponechá aktuální stav grafiky přesně takový, jaký byl, jako by tato instrukce byla no-op, takže zbytek proudu pokračuje v přehrávání na rozumném stavu a volající se pak stále může rozhodnout, z počtu, zda vstup stojí za nahlášení tomu, kdo jej vyprodukoval
Skládání CTM, nezávislost textové matice na q/Q, a stupňovité uskutečnění ořezové cesty nezávisí na tom, jak nebo zda se proud obsahu kdy vymaluje, což je přesně ten bod: stejný snímek TPDFContentStateTracker je správný ať se stránka nikdy nevykreslí vůbec, nebo se právě chystá předat jakémukoli backendu, který PDFlibPas pro tento soubor zvolí, včetně přepínání enginu za běhu popsaného v průvodci vícemotorovým vykreslováním PDF v PDFlibPas. Analýza obsahu, mapování souřadnic a nástroje pro redakci mohou všechny běžet zcela na výstupu trackeru, dlouho předtím, nebo úplně bez toho, aby se kdy vykreslovač musel zapojit
Přehrávání proudu obsahu přes TPDFContentStateTracker je součástí strukturovaného rámce editace obsahu vestavěného do PDFlibPas, nativní knihovny komponenty PDF pro VCL pro Delphi a C++Builder