Technický článek

Stav proudu obsahu v PDF Library for Delphi: sledování CTM a ořezu

PDF Library for Delphi, 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

Diagram PDF Library for Delphi pipeline přehrávání obsahového streamu: parsované operátory PDF předávají TPDFContentStateTracker po jednom operátoru, který udržuje CTM, textovou matici, meze ořezu a zásobník q/Q jako záznam snímku
Každý načtený operátor aktualizuje záznam grafického stavu ve trackeru a jeden snapshot na instrukci vznikne v jediném lineárním průchodu. Záznam zůstává použitelný bez jakéhokoli vykreslovacího plátna

Proč přehrávat proud obsahu místo jeho vykreslení?

PDF Library for Delphi 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;
    // Jeden snímek před instrukcí pro každý operátor, vypočtený v jediném průchodu
    States := Prog.TraceGraphicsStates(nil, False);
    for I := 0 to High(Runs) do
    begin
      // OriginX/OriginY už zahrnují Tm/Td/TD/T*; chybí jen aktivní CTM
      // v této instrukci (ISO 32000-1 8.4) stále chybí
      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 PDF Library for Delphi 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

Časová osa PDF Library for Delphi ukázkového streamu BT, Td, q, cm, Tj, Q, Tj, ET se stavovou tabulkou ukazující, že Q zruší škálování CTM, zatímco DX 100 textové matice přežije oba běhy glyfů
cm uvnitř páru q/Q zdvojnásobí CTM jen pro první běh glyphů a Q toto měřítko opět odstraní. Textová matice nastavená Td přežije, protože obnovení stavu ji znovu aplikuje, místo aby věřila odloženému záznamu grafického stavu
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ý PDF Library for Delphi 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

PDF Library for Delphi: Dvoukroková časová osa ořezu, kde W zaznamená jen pravidlo ořezu, následující operátor malování cesty protne čekající meze a ClipBoundsExact zůstane true jen pro jediný obdélník re
Ořez se projeví, až když běží další operátor malující cestu, takže snímek před W stále hlásí starší, širší meze. Pouze jeden na osu orientovaný re dá ClipBoundsExact true, zatímco vše ostatní poctivě degraduje na bezpečnou vnější mez

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. PDF Library for Delphi 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
  // Stejný index instrukce, dva různé okamžiky: před jejím a po jejím provedení
  Prog.GetGraphicsState(CmIndex, False, Before);
  Prog.GetGraphicsState(CmIndex, True, After);
  // Before.CTM odráží každé předchozí cm; After.CTM už zahrnuje
  // také konkatenaci této instrukce
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ý PDF Library for Delphi 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 PDF Library for Delphi. 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 PDF Library for Delphi, nativní knihovny komponenty PDF pro VCL pro Delphi a C++Builder