Technical Article

Ako funguje grafika v PDF: Obsahové prúdy a operátory

Stránka PDF neukladá pixely a neukladá ani strom tvarových objektov ako formát SVG. Ukladá program. Každá čiara, krivka, výplň a umiestnený obrázok na stránke sú výsledkom vykonania sekvencie operátorov v obsahovom prúde (content stream) zhora nadol voči aktuálnemu stavu grafiky. Pochopte túto jednu skutočnosť a väčšina správania tohto formátu vás prestane prekvapovať: prečo výplň vyžaduje po zostavení cesty samostatný operátor kreslenia, prečo farby a šírky čiar unikajú z jedného tvaru do druhého, ak ich neohraničíte, a prečo ten istý kresliaci kód môže po jedinej transformácii súradníc skončiť na úplne inom mieste. Toto je prehliadka tohto modelu vykonávania tak, ako ho definuje norma ISO 32000: operátory, s ktorými sa stretnete pri otvorení obsahového prúdu, a pravidlá, ktoré rozhodujú o tom, čo sa zobrazí na stránke.

Obsahový prúd je postfixový bajtkód

Obsahový prúd je plochá bajtová sekvencia operandov sprevádzaných operátormi. Najprv idú operandy, ako posledný ide operátor, ktorý ich spracuje. Ide o opačný princíp než pri volaní funkcie a zhoduje sa s fungovaním zásobníkového stroja: vložte čísla a potom zadajte príkaz. Neexistuje tu žiadne vnorenie, syntaktické výrazy ani premenné. Obrys trojuholníka predstavuje týchto päť riadkov:

100 100 m    % moveto: start a new subpath at (100, 100)
200 200 l    % lineto: add a segment to (200, 200)
300 100 l    % lineto: add a segment to (300, 100)
h            % closepath: connect back to the start
S            % stroke: paint the path outline

Operátory sú zámerne stručné. Reálna stránka obsahuje tisíce takýchto príkazov, zvyčajne komprimovaných pomocou FlateDecode. Daňou za túto kompaktnosť je, že prúd nenesie žiadnu štruktúru, na ktorú by ste sa mohli opýtať: prehliadač sa nemôže opýtať, „kde je nadpis na tejto stránke“, môže iba spustiť program a vykresliť to, čo dostane. To je hlavný dôvod, prečo je extrakcia textu z ľubovoľných PDF zložitá.

Počiatok súradníc je vľavo dole a os Y rastie nahor

Predtým, ako začnú akékoľvek súradnice dávať zmysel, musíte vedieť, kde sa nachádza bod (0, 0). PDF umiestňuje počiatok súradníc do ľavého dolného rohu stránky, pričom os X rastie doprava a os Y rastie nahor. Meria sa v bodoch, pričom 72 bodov zodpovedá jednému palcu (ISO 32000-2 §8.3.2). Na stránke formátu US Letter sa horný okraj nachádza na súradnici y = 792, nie y = 0. Každý, kto prichádza z oblasti obrazovkovej grafiky, kde je počiatok vľavo hore a os Y rastie nadol, sa na prvýkrát pomýli a nakreslí prvú čiaru mimo spodného okraja stránky. Jednotka je nezávislá od média: 72 jednotiek predstavuje jeden palec bez ohľadu na to, či sa stránka vykresľuje na obrazovku telefónu alebo na osvetľovaciu jednotku.

Väčšina knižníc na kreslenie stránok túto konvenciu preberá priamo. V HotPDF sa napríklad TextOut a volania ciest merajú od ľavého dolného rohu v bodoch, takže hodnota blízka výške stránky umiestni obsah na samotný vrch:

// HotPDF, Delphi: y measured from the bottom edge upward, in points
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700);   // near the top of the page
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke;             // emits the moveto/lineto/stroke operators

Táto sekvencia volaní sa skompiluje presne do vyššie uvedených operátorov m, l a S. Knižnica je len zapisovačom pre obsahový prúd, ničím viac, a znalosť toho, čo emituje, vám pomôže pochopiť výstup, keď tvar skončí inde, než ste čakali.

Zostavte cestu a potom ju vykreslite

PDF oddeľuje konštrukciu cesty od jej kreslenia, a toto oddelenie nie je len formálnou záležitosťou. Najprv popíšete tvar pomocou konštrukčných operátorov, ktoré na stránku nepridajú nič viditeľné, a potom zadáte jediný kresliaci operátor, ktorý rozhodne, čo s vytvorenou cestou urobí. Rovnaký trojuholník môže byť obrysom, plnou výplňou alebo oboma naraz, a to len na základe toho, akým príkazom ho ukončíte.

Konštrukčných operátorov je málo. m začína novú podcestu v danom bode. l pridáva rovný segment. c pridáva kubickú Bezierovu krivku zo šiestich operandov (dva riadiace body a koncový bod). re je skratka, ktorá pridá celý obdĺžnik definovaný súradnicami x, y, šírkou a výškou. h uzatvára aktuálnu podcestu smerom k jej začiatku. Žiadny z nich nekreslí na stránku, iba zhromažďujú geometrické údaje.

200 250 m                    % start the subpath
300 350 400 450 500 250 c    % cubic Bezier: two control points, then endpoint
150 200 re                   % a 150 x 200 rectangle, added as its own subpath
h                            % close

Pôvodný príklad používal dnes už zastaraný variant krivky y. Operátor c s tromi explicitnými bodmi je formou, s ktorou sa stretnete v praxi a ktorú by ste mali používať. Akonáhle cesta existuje, jeden kresliaci operátor ju dokončí. Slovná zásoba je malá a oplatí sa ju zapamätať, pretože každý tvar na každej strane končí jedným z nich:

  • S vykreslí obrys cesty (stroke) s použitím aktuálnej šírky čiary a farby obrysu.
  • f vyplní vnútro (fill) s použitím aktuálnej farby výplne a pravidla nenulového vinutia (nonzero winding rule).
  • f* vyplní vnútro s použitím pravidla párny-nepárny (even-odd rule), čo je dôležité pri pretínajúcich sa tvaroch a tvaroch s otvormi.
  • B vyplní a následne vykreslí obrys v rámci jednej operácie; b cestu najprv uzavrie.
  • n nevykreslí nič, čím sa cesta stane orezávacou oblasťou (clip region) bez zanechania viditeľnej stopy.

Pravidlo vinutia (winding rule) býva častým zdrojom chýb. Nonzero (f, B) počíta znamienkové priesečníky lúča z testovacieho bodu a vypĺňa všade, kde výsledok nie je nula. Otvor tak zostane prázdny iba vtedy, ak sa jeho podcesta vinie opačným smerom ako vonkajšia podcesta. Even-odd (f*, B*) prepína stav pri každom prechode bez ohľadu na smer. Ak tvar „šišky“ vyjde ako plný, vnútorný kruh sa vinie rovnakým smerom ako vonkajší, a vy ho musíte buď otočiť, alebo prepnúť na pravidlo even-odd.

Farba je režim, nie parameter

Farba v obsahovom prúde je perzistentná (lepkavá). Nastavíte farbu a tá zostane aktívna, kým nenastavíte inú alebo neobnovíte predchádzajúci stav. Preto neohraničená zmena farby potichu zafarbí všetko, čo sa kreslí po nej. PDF tiež drží farbu výplne a farbu obrysu as dve nezávislé nastavenia, pričom na výplň používa operátory malými písmenami a na obrys veľkými písmenami. Zariadením definované farebné priestory (device color spaces) majú svoje vlastné skratky:

0.5 g                % DeviceGray fill, mid gray (0 = black, 1 = white)
0.2 0.6 0.8 rg       % DeviceRGB fill
0.8 0.2 0.1 RG       % DeviceRGB stroke (uppercase = stroke)
0.2 0.8 0.0 0.1 k    % DeviceCMYK fill

DeviceRGB je vhodný pre obrazovkový výstup, DeviceCMYK očakáva tlačová produkcia a DeviceGray je najúspornejšou voľbou pre jednofarebný obsah. Tieto priestory sú pohodlné, ale nekalibrované: rovnaká trojica RGB sa môže na dvoch rôznych monitoroch vykresliť odlišne. A to je problém, ktorý riešia farebné priestory založené na ICC a výstupné profily PDF/A. Pre prácu citlivú na vernosť farieb volíte kalibrovaný priestor pomocou cs a CS a zložky nastavujete pomocou sc a scn. Pre bežné dokumenty však zariadením definované skratky postačujú. Knižnica tieto príkazy balí do typových volaní. HotPDF napríklad berie jeden TColor a emituje zodpovedajúce operátory:

Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150);  // x, y, width, height
Pdf.CurrentPage.Fill;

Pdf.CurrentPage.SetRGBFillColor(RGB(0, 255, 0));
Pdf.CurrentPage.Circle(150, 400, 50);           // x, y, radius
Pdf.CurrentPage.Fill;

Stav grafiky a zásobník q/Q

Všetko, čo nie je samotnou cestou, žije v stave grafiky (graphics state): aktuálna matica transformácie, farby výplne a obrysu, šírka čiary, vzor prerušovania, orezávacia oblasť či alfa kanál. Tento stav je globálny a meniteľný, takže jediným bezpečným spôsobom, ako urobiť lokálnu zmenu, je uložiť celok, upraviť ho, nakresliť tvar a stav obnoviť. Presne to robia operátory q a Q. Operátor q vloží kópiu aktuálneho stavu na zásobník a Q ju vybere, čím zahodí každú zmenu vykonanú od zodpovedajúceho volania q.

q                    % save the entire graphics state
2 0 0 2 100 100 cm   % concatenate a transform: scale 2x, translate to (100,100)
0.8 g                % gray fill, scoped to this block
% ... draw scaled, gray content ...
Q                    % restore: transform and color revert

Nevyvážené volania q a Q sú častým spôsobom, ako sa ručne vytvorený alebo spojený obsahový prúd poškodí. Zabudnutý operátor q bez prislúchajúceho Q ponechá zásobník plný aj po skončení stránky. Nadbytočný operátor Q spôsobí jeho podtečenie. V oboch prípadoch môže prehliadač ponechať v platnosti staré orezanie alebo transformáciu, kvôli čomu obsah zmizne alebo skončí na nesprávnom mieste. Keď grafika zmizne bez zjavného geometrického dôvodu, skontrolujte najprv zásobník stavov.

Matica CTM transformuje každú súradnicu

Aktuálna matica transformácie (current transformation matrix, CTM) stojí medzi číslami vo vašich operátoroch a samotnou stránkou. Každá súradnica sa pred kreslením vynásobí maticou CTM, takže zmena tejto matice zmení miesto a spôsob zobrazenia všetkých nasledujúcich kresieb bez toho, aby ste sa dotkli jediného koordinátu cesty. Operátor cm spája novú maticu s tou aktuálnou, pričom berie šesť operandov zodpovedajúcich afinnému zobrazeniu [a b c d e f]:

1 0 0 1 100 50 cm        % translate by (100, 50): e and f carry the offset
2 0 0 1.5 0 0 cm         % scale x by 2, y by 1.5: a and d are the scale factors
0.707 0.707 -0.707 0.707 0 0 cm   % rotate 45 degrees (cos/sin in a, b, c, d)

Dve veci zvyknú spôsobovať problémy. Po prvé, operátor cm matice spája a nenahrádza ich, takže transformácie sa akumulujú a záleží na ich poradí (zmena mierky a následný posun nie je to isté ako posun a následná zmena mierky). Po druhé, rotácia a zmena mierky prebiehajú okolo aktuálneho počiatku súradníc, nie okolo stredu vášho tvaru. Ak chcete niečo otočiť na mieste, musíte to posunúť do počiatku súradníc, otočiť, a posunúť späť, to všetko uzavreté v bloku q/Q. Táto istá matica slúži aj na umiestňovanie obrázkov, čo je posledná dôležitá časť.

Obrázky a opakovane použiteľný obsah sú XObjekty

Rastové obrázky nežijú priamo v obsahovom prúde. Sú ukladatelné ako XObjekty obrázka (image XObjects), čo sú externé objekty s vlastným slovníkom popisujúcim šírku, výšku, bitovú hĺbku, farebný priestor a filter kompresie. Obsahový prúd na ne iba odkazuje. Fotografia komprimovaná ako JPEG sa deklaruje takto:

/Photo <<
  /Type /XObject
  /Subtype /Image
  /Width 640
  /Height 480
  /BitsPerComponent 8
  /ColorSpace /DeviceRGB
  /Filter /DCTDecode        % the image data is a JPEG stream
>>

XObjekt obrázka sa kreslí do jednotkového štvorca: v používateľskom priestore vždy zaberá oblasť od (0, 0) do (1, 1). Nezadávate mu pozíciu ani rozmer. Namiesto toho nastavíte maticu CTM tak, aby sa jednotkový štvorec namapoval na požadovaný obdĺžnik, a potom ho vyvoláte operátorom Do. Preto je umiestnenie obrázka vždy záležitosťou transformácie nasledovanej vyvolaním, uzavretej v bloku save/restore, aby mierka neovplyvnila ďalšie kreslenie:

q
640 0 0 480 50 300 cm    % map the unit square to a 640x480 box at (50, 300)
/Photo Do                % paint the image XObject
Q

Rovnaký mechanizmus Do poháňa aj XObjekty formulárov (form XObjects), ktoré nesú opakovane použiteľný kus grafiky (logo alebo opakujúcu sa pečiatku) ako samostatný obsahový prúd s ohraničujúcim boxom (bounding box). Definujete ho raz, vyvoláte mnohokrát s rôznou maticou CTM a bajty sa v súbore vyskytnú len raz. Väčšina knižníc to skrýva za jediné volanie umiestnenia: HotPDF registruje bitmapu pomocou AddImage a umiestňuje ju cez ShowImage, pričom prijíma explicitné x, y, šírku a výšku namiesto toho, aby od vás žiadal manuálne zostavenie matice:

var
  Bmp: TBitmap;
  ImgIndex: Integer;
begin
  Bmp := TBitmap.Create;
  try
    Bmp.LoadFromFile('logo.bmp');
    ImgIndex := Pdf.AddImage(Bmp, icFlate);
    // x, y (bottom-left), width, height, rotation angle
    Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
  finally
    Bmp.Free;
  end;
end;

Pod týmto jedným riadkom knižnica zapíše slovník XObjektu obrázka, nastaví CTM na rozmer a pozíciu jednotkového štvorca a emituje operátor Do. Model pod tým sa oplatí poznať, pretože vysvetľuje každý zvláštny výsledok: natiahnutý obrázok je dôsledkom nesprávnych faktorov mierky v CTM, rovnaké logo na štyridsiatich stranách je jeden XObjekt formulára vyvolaný štyridsaťkrát a obrázok vykreslený hore nohami značí prehodenie znamienka v matici, nie poškodený súbor.

Kam to vedie

Grafický model je jednoduchý, akonáhle pochopíte jeho štruktúru. Obsahový prúd je postfixový bajtkód bežiaci voči meniteľnému stavu. Súradnice začínajú vľavo dole a prechádzajú cez CTM. Cesty sa vytvárajú na pozadí a vykresľujú sa jedným konkrétnym operátorom. Nastavenia farieb a čiar pretrvávajú, kým ich neohraničíte pomocou q/Q. Obrázky a opakovane použiteľná grafika sú XObjekty umiestnené transformáciou jednotkového štvorca. Takmer každý mätúci výsledok vykresľovania sa redukuje na jedno z týchto piatich pravidiel. Ak chcete vidieť, ako tieto grafické operátory sedia vo väčšom objektovom modeli (slovníky stránok a tabuľka krížových odkazov), technický prehľad štruktúry súborov PDF sa venuje tejto vrstve a článok o budovaní jednoduchého PDF od nuly prechádza bajty od začiatku do konca. Kreslenie textu žije vo vlastnej rodine operátorov a má vlastné úskalia, ktorým sa venuje samostatný článok o práci s textom a písmami v PDF.

Tu zobrazené kresliace volania v Delphi (MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage a ShowImage) sú súčasťou komponentu HotPDF pre Delphi a C++Builder, ktorý tieto operátory obsahového prúdu emituje za vás.