Műszaki cikk

Hogyan működnek a PDF grafikák: tartalomfolyamok (content streams) és operátorok

Egy PDF oldal nem pixeleket tárol, és nem is alakzat (shape) objektumok fáját tárolja úgy, ahogy az SVG teszi. Egy programot tárol. Minden vonal, görbe, kitöltés (fill) és elhelyezett kép az oldalon egy operátor-szekvencia (sequence of operators) végrehajtásának eredménye egy tartalomfolyamban, fentről lefelé, egy futó grafikai állapottal (graphics state) szemben. Értse meg ezt az egy tényt, és a formátum viselkedésének nagy része többé nem lesz meglepő: miért van szüksége egy kitöltésnek külön festő operátorra (painting operator) az útvonal (path) megépítése után, miért szivárognak át (leak) a színek és a vonalvastagságok az egyik alakzatból a másikba, hacsak nem zárójelezi (bracket) őket, miért landolhat ugyanaz a rajzkód teljesen különböző helyeken egyetlen koordináta transzformáció után. Ez annak a végrehajtási modellnek a bemutatása, ahogyan azt az ISO 32000 meghatározza: az operátorok, amelyekkel találkozik egy tartalomfolyam megnyitásakor, és a szabályok, amelyek eldöntik, hogy mi jelenik meg az oldalon

A tartalomfolyam (content stream) posztfix bájtkód (postfix bytecode)

A tartalomfolyam operandusok, majd azokat követő operátorok lapos (flat) bájtsorozata. Az operandusok jönnek először, az operátor, amely elfogyasztja őket, jön utoljára; ez egy függvényhívás fordítottja, és megegyezik egy veremgép (stack machine) működésével: tegye be a számokat (push the numbers), majd adja ki az igét (issue the verb). Nincs beágyazás (nesting), nincs kifejezés szintaxis (expression syntax), nincsenek változók. Egy háromszög körvonala (triangle outline) öt sor ebből:

100 100 m    % moveto: új részútvonal (subpath) indítása a (100, 100) pontban
200 200 l    % lineto: szegmens hozzáadása a (200, 200) ponthoz
300 100 l    % lineto: szegmens hozzáadása a (300, 100) ponthoz
h            % closepath: visszacsatolás a kezdethez
S            % stroke: az útvonal körvonalának (path outline) megfestése

Az operátorok szándékosan tömörek (terse). Egy valódi oldal több ezer ilyet tartalmaz, általában FlateDecode-dal tömörítve. Ennek a kompaktságnak (compactness) az az ára, hogy a folyam nem hordoz lekérdezhető struktúrát: a megjelenítő (viewer) nem tudja megkérdezni, hogy "hol van a címsor ezen az oldalon", csak le tudja futtatni a programot, és megnézni, hogy milyen tinta hova kerül (what ink lands where). Ez a gyökéroka annak, hogy a szövegkinyerés (text extraction) tetszőleges PDF-ekből miért nehéz

Az origó bal alul van, és az Y felfelé növekszik

Mielőtt bármilyen koordinátának értelme lenne, tudnia kell, hol van a (0, 0). A PDF az origót az oldal bal alsó sarkába helyezi, az X jobbra, az Y pedig felfelé növekszik, pontokban (points) mérve, 72 pont per hüvelyk (ISO 32000-2 §8.3.2) arányban. Egy US Letter oldalon a felső él y = 792-nél van, nem pedig y = 0-nál. Bárki, aki a képernyő grafikák (screen graphics) felől érkezik, ahol az origó bal felül van, és az Y lefelé növekszik, elsőre fordítva csinálja (gets this backwards), és az első vonalat az oldal aljáról lefelé (off the bottom of the page) húzza meg. A mértékegység a médiumtól is független: 72 egység egy hüvelyk, függetlenül attól, hogy az oldal egy telefon képernyőjén vagy egy levilágítón (imagesetter) jelenik meg

A legtöbb oldal-rajzoló könyvtár (page-drawing libraries) közvetlenül örökli ezt a konvenciót (convention). A HotPDF-ben például a TextOut és az útvonal (path) hívások mind a bal alsó sarokból mérnek pontokban, így egy az oldal magasságához közeli érték a tartalmat felülre helyezi:

// HotPDF, Delphi: az y az alsó éltől felfelé mérve, pontokban (points)
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700);   // az oldal teteje közelében
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke;             // kibocsátja (emits) a moveto/lineto/stroke operátorokat

Ez a hívási szekvencia (call sequence) pontosan a fenti m, l és S operátorokká fordul le (compiles down). A könyvtár csak egy gépíró (typist) a tartalomfolyam számára, semmi több, és az, hogy tudja, mit bocsát ki, lehetővé teszi, hogy megértse (reason about) a kimenetet, amikor egy alakzat (shape) olyan helyen landol, ahol nem várta

Építse meg az útvonalat (path), majd fesse meg (paint it)

A PDF elválasztja az útvonal építését (path construction) az útvonal festésétől (path painting), és a szétválasztás nem pedantéria. Először építő operátorokkal (construction operators) írja le az alakzatot, amelyek semmi láthatót nem adnak hozzá, majd kiad egyetlen festő operátort, amely eldönti, mit kezdjen a felhalmozott (accumulated) útvonallal. Ugyanaz a háromszög lehet körvonal (outline), egyszínű kitöltés (solid fill) vagy mindkettő, attól függően, hogy milyen igével (verb) fejezi be

Kevés építő operátor (construction operators) létezik. Az m egy új részútvonalat (subpath) indít egy pontban. Az l egy egyenes szegmenst (straight segment) ad hozzá. A c egy köbös Bezier-görbét (cubic Bezier curve) ad hozzá hat operandusból, két vezérlőpontból (control points) és egy végpontból. A re egy parancsikon (shortcut), amely egy teljes téglalapot (rectangle) ad hozzá egy x, y, szélesség, magasság (width, height) négyesből (quadruple). A h lezárja a jelenlegi részútvonalat, visszatérve annak kezdetéhez. Egyikük sem tesz tintát (ink) az oldalra; csak a geometriát halmozzák fel (accumulate geometry)

200 250 m                    % a részútvonal (subpath) indítása
300 350 400 450 500 250 c    % köbös Bezier: két vezérlőpont, majd a végpont
150 200 re                   % egy 150 x 200-as téglalap, önálló részútvonalként hozzáadva
h                            % bezárás

Az eredeti példa a görbe (curve) operátor mára elavult y változatát használta; a c a maga három explicit pontjával az a forma, amelyet a gyakorlatban látni fog, és amelyet használnia érdemes (reach for). Miután az útvonal (path) létrejött, egy festő operátor (painting operator) fejezi be azt. A szókincs (vocabulary) kicsi, és érdemes megjegyezni, mert minden oldalon minden alakzat ezek valamelyikével végződik:

  • Az S kihúzza (strokes) az útvonal körvonalát az aktuális vonalvastagság és körvonalszín (stroke color) használatával
  • Az f kitölti a belsejét (interior) az aktuális kitöltési szín (fill color) és a nem nulla körüljárási szabály (nonzero winding rule) használatával
  • Az f* a páros-páratlan (even-odd) szabályt használva tölti ki, ami az önmagukat metsző (self-intersecting) alakzatok és a lyukas alakzatok (shapes with holes) esetében számít
  • A B egyetlen műveletben tölt ki (fills) és húz ki (strokes); a b először lezárja az útvonalat
  • Az n nem fest semmit, így válik egy útvonal vágási régióvá (clip region) anélkül, hogy látható nyomot (visible mark) hagyna

A körüljárási szabály (winding rule) az a rész, amelyet az emberek el szoktak rontani. A nem nulla (nonzero, f, B) egy tesztpontból kiinduló sugár (ray) előjeles (signed) metszéspontjait (crossings) számolja, és mindenhol kitölt, ahol a szám (count) nem nulla, így egy lyuk csak akkor marad üres, ha annak részútvonala (subpath) a külsővel ellentétes irányba (winds opposite) halad. A páros-páratlan (even-odd, f*, B*) minden metszésponton vált (toggles), iránytól függetlenül. Ha egy "fánk" (donut) alakzat tömörként (solid) jelenik meg, akkor a belső kört ugyanúgy járták körül (wound), mint a külsőt, és ön vagy megfordítja azt, vagy átvált páros-páratlanra

A szín egy mód, nem pedig paraméter

A szín egy tartalomfolyamban (content stream) ragadós (sticky). Beállít egy színt, és az beállítva marad, amíg be nem állít egy másikat, vagy vissza nem állít egy korábbi állapotot, ezért van az, hogy egy nem zárójelezett (unbracketed) színváltoztatás csendben (silently) beszínezi (tints) az utána rajzolt összes dolgot. A PDF a kitöltési színt (fill color) és a körvonal színét (stroke color) két független beállításként kezeli, kisbetűs (lowercase) operátorokkal a kitöltéshez és nagybetűsekkel (uppercase) a körvonalhoz. Az eszköz színterek (device color spaces) mindegyike rendelkezik saját rövidítéssel (shorthand):

0.5 g                % DeviceGray kitöltés, középszürke (mid gray) (0 = fekete, 1 = fehér)
0.2 0.6 0.8 rg       % DeviceRGB kitöltés
0.8 0.2 0.1 RG       % DeviceRGB körvonal (stroke) (nagybetűs = körvonal)
0.2 0.8 0.0 0.1 k    % DeviceCMYK kitöltés

A DeviceRGB megfelel a képernyő kimenetnek (screen output), a DeviceCMYK az, amit a nyomdai gyártás (print production) elvár, és a DeviceGray a legkisebb választás a monokróm tartalmakhoz. Az eszköz színterek kényelmesek, de kalibrálatlanok (uncalibrated): ugyanaz az RGB hármas két monitoron eltérően jelenhet meg, és pontosan ezt a problémát hivatottak megoldani az ICC-alapú színterek (ICC-based color spaces) és a PDF/A kimeneti szándékok (output intents). Színkritikus (color-critical) munkához egy kalibrált teret választ a cs és CS segítségével, a komponenseket pedig az sc és scn segítségével állítja be, de az átlagos dokumentumok esetében az eszköz rövidítései (device shorthands) viszik a terhet (carry the load). Egy könyvtár ezeket típusos hívásokba (typed calls) csomagolja (wraps). A HotPDF például egyetlen TColor értéket vesz fel, és kibocsátja a megfelelő operátorokat:

Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150);  // x, y, szélesség, magasság
Pdf.CurrentPage.Fill;

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

A grafikai állapot (graphics state) és a q/Q verem (stack)

Minden, ami nem maga az útvonal (path), a grafikai állapotban él: az aktuális transzformációs mátrix (current transformation matrix), a kitöltő és körvonal színek, a vonalvastagság (line width), a szaggatott minta (dash pattern), a vágási régió (clip region), az alfa. Az állapot globális és módosítható (mutable), ezért a lokális változtatás egyetlen biztonságos módja az, ha elmenti az egészet, módosítja, megrajzolja, majd visszagörgeti (roll it back). Ezt teszi a q és a Q. A q ráteszi (pushes) a jelenlegi állapot másolatát egy veremre (stack); a Q leveszi (pops) azt, eldobva minden olyan változást, amelyet a hozzá tartozó q óta hajtottak végre

q                    % a teljes grafikai állapot mentése
2 0 0 2 100 100 cm   % egy transzformáció összefűzése (concatenate): 2x-es nagyítás (scale), eltolás a (100,100) ponthoz
0.8 g                % szürke kitöltés, erre a blokkra érvényes (scoped)
% ... méretezett (scaled), szürke tartalom rajzolása ...
Q                    % visszaállítás (restore): a transzformáció és a szín visszatér (revert)

A kiegyensúlyozatlan (unbalanced) q és Q gyakori oka annak, hogy egy kézzel épített (hand-built) vagy összefűzött (stitched) tartalomfolyam elromlik. Egy eltévedt (stray) q, amelyhez nem tartozik megfelelő Q, mélyen hagyja a vermet (stack), amikor az oldal véget ér; egy extra Q pedig alulcsordulást (underflows) okoz rajta. Így vagy úgy, egy megjelenítő (viewer) érvényben (in force) tarthat egy régi vágást (clip) vagy transzformációt, a tartalom pedig eltűnik, vagy rossz helyen landol. Amikor a grafika minden olyan ok nélkül tűnik el (vanish), amelyet az útvonal (path) meg tudna magyarázni, először az állapot vermet (state stack) vizsgálja meg (audit)

A CTM minden koordinátát transzformál

Az aktuális transzformációs mátrix (current transformation matrix, CTM) az operátorokban lévő számok és a tényleges oldal között helyezkedik el. Minden koordinátát megszoroznak a CTM-mel, mielőtt bármit is megrajzolnának, így a mátrix megváltoztatása megváltoztatja, hogy hol és hogyan jelenjen meg minden későbbi rajz anélkül, hogy egyetlen útvonalkoordinátát (path coordinate) is érintene. A cm operátor egy új mátrixot fűz hozzá (concatenates) az aktuálishoz, felvéve hat operandust, amelyek az [a b c d e f] affin mátrixra (affine matrix) képeződnek le (map to):

1 0 0 1 100 50 cm        % eltolás (translate) (100, 50) értékkel: az e és f hordozza az eltolást (offset)
2 0 0 1.5 0 0 cm         % x skálázása (scale) 2-vel, y 1,5-tel: az a és d a skálatényezők (scale factors)
0.707 0.707 -0.707 0.707 0 0 cm   % 45 fokos forgatás (rotate) (cos/sin az a, b, c, d-ben)

Két dolog szokta megbotlasztani (trip people up) az embereket. Először is, a cm inkább komponál (composes), mintsem helyettesít, így a transzformációk felhalmozódnak (accumulate), és a sorrend számít: a skálázás majd eltolás (translating) nem ugyanaz, mint az eltolás majd skálázás. Másodszor, a forgatás és a skálázás az aktuális origó körül fordul (pivot around), nem pedig az alakzat középpontja körül, így ahhoz, hogy valamit a helyén (in place) forgasson el, eltolja azt az origóba, elforgatja, majd visszatolja, mindezt a q/Q-ba csomagolva. Ugyanez a mátrix az, amely a képeket elhelyezi, és ez az utolsó rész (piece), amelyet érdemes megnézni

A képek és az újrahasználható tartalmak XObjectek

A raszterképek (Raster images) nem beágyazva (inline) élnek a tartalomfolyamban. Kép XObjectekként (image XObjects) vannak tárolva, külső objektumokként, amelyek saját szótárral rendelkeznek, amely leírja a szélességet, magasságot, bitmélységet (bit depth), színteret és tömörítési szűrőt (compression filter), a tartalomfolyam pedig csak hivatkozik rájuk. Egy JPEG-alapú fotó (JPEG-backed photo) így deklarálja magát:

/Photo <<
  /Type /XObject
  /Subtype /Image
  /Width 640
  /Height 480
  /BitsPerComponent 8
  /ColorSpace /DeviceRGB
  /Filter /DCTDecode        % a képadat egy JPEG folyam
>>

Egy kép XObject az egységnégyzetbe (unit square) rajzol: mindig a (0, 0)-tól az (1, 1)-ig terjedő régiót (region) foglalja el a felhasználói térben (user space). Ön nem ad át neki pozíciót vagy méretet. Ehelyett úgy állítja be a CTM-et, hogy az egységnégyzet (unit square) a kívánt téglalapra (rectangle) képeződjön le (maps), majd a Do paranccsal hívja meg (invoke). Ezért a kép elhelyezése mindig egy transzformáció, amelyet egy meghívás követ, save/restore-ba (mentés/visszaállítás) csomagolva, hogy a skála (scale) ne vérezzen át (bleed into) a következő műveletre:

q
640 0 0 480 50 300 cm    % az egységnégyzet leképezése egy 640x480-as dobozra (box) a (50, 300) pontban
/Photo Do                % a kép XObject megfestése
Q

Ugyanez a Do mechanizmus hajtja (drives) a form XObjecteket, amelyek grafikák egy újrahasználható darabját (reusable chunk), egy logót vagy egy ismétlődő bélyegzőt (repeated stamp) tárolnak saját tartalomfolyamukként, egy határoló dobozzal (bounding box). Definiálja egyszer, hívja meg (invoke) sokszor egy különböző CTM-mel, és a bájtok csak egyszer jelennek meg a fájlban. A legtöbb könyvtár ezt egyetlen elhelyezési hívás (placement call) mögé rejti: a HotPDF az AddImage-dzsel regisztrál egy bittérképet (bitmap), és a ShowImage-dzsel helyezi el, explicit x, y, szélesség és magasság értékeket adva meg ahelyett, hogy arra kérné önt, hogy kézzel építse fel a mátrixot:

var
  Bmp: TBitmap;
  ImgIndex: Integer;
begin
  Bmp := TBitmap.Create;
  try
    Bmp.LoadFromFile('logo.bmp');
    ImgIndex := Pdf.AddImage(Bmp, icFlate);
    // x, y (bal alsó), szélesség, magasság, forgatási szög
    Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
  finally
    Bmp.Free;
  end;
end;

Ez alatt az egy sor alatt a könyvtár megírja a kép XObject szótárat (dictionary), beállítja a CTM-et az egységnégyzet méretezéséhez és pozicionálásához, majd kibocsátja (emits) a Do parancsot. A mögöttes modellt (model underneath) érdemes ismerni, mert megmagyaráz minden furcsa (odd) eredményt: egy megnyújtott (stretched) kép egy olyan CTM, amelynél nem egyeznek a skálatényezők (scale factors), a negyven oldalon azonos logó egy form XObject negyvenszer meghívva, egy fejjel lefelé (upside down) renderelt kép pedig egy előjelváltás (sign flip) a mátrixban, nem pedig egy sérült (corrupt) fájl

Hová vezet ez (Where this leads)

A grafikai modell (graphics model) kicsi, ha egyszer meglátja az alakját. A tartalomfolyam (content stream) egy módosítható állapottal (mutable state) szemben futó posztfix bájtkód (postfix bytecode); a koordináták bal alulról indulnak, és áthaladnak a CTM-en; az útvonalak (paths) csendben (silently) épülnek fel, és egyetlen szándékos (deliberate) operátorral festik meg őket; a szín és a vonal beállítások mindaddig megmaradnak, amíg q/Q-val nem zárójelezi (bracket) őket; a képek és az újrahasználható grafikák olyan XObjectek, amelyeket egy egységnégyzet (unit square) transzformálásával helyeznek el. Szinte minden zavaró megjelenítési (rendering) eredmény ezen öt szabály valamelyikére vezethető vissza (reduces to). Ha látni szeretné, hogyan helyezkednek el ezek a grafikai operátorok a nagyobb objektummodellen (object model) belül, az oldalszótárakat és a rájuk mutató kereszthivatkozási táblát (cross-reference table), akkor a PDF fájlstruktúra műszaki áttekintése tárgyalja ezt a réteget, a PDF dokumentum építése az alapoktól pedig a bájtokon halad végig az elejétől a végéig. A szövegrajzolás (text drawing) a saját operátorcsaládjában él, és megvannak a saját csapdái (pitfalls), amelyeket a PDF szöveg- és betűtípus-kezelés című kísérő cikk (companion piece) tárgyal

Az itt látható Delphi rajzhívások, a MoveTo, a LineTo, a Stroke, a Rectangle, a Fill, a SetRGBFillColor, az AddImage és a ShowImage a Delphihez és C++Builderhez készült HotPDF Component részét képezik, amely kibocsátja (emits) ön helyett ezeket a tartalomfolyam (content-stream) operátorokat