Technical Article

Kako funkcioniše PDF grafika: tokovi sadržaja i operatori

PDF stranica ne čuva piksele niti čuva stablo objekata oblika na način na koji to radi SVG. Ona čuva program. Svaka linija, kriva, popunjavanje i postavljena slika na stranici rezultat su izvršavanja niza operatora u toku sadržaja (content stream), od vrha do dna, u odnosu na aktivno grafičko stanje. Kada razumete tu jednu činjenicu, većina ponašanja ovog formata prestaje da vas iznenađuje: zašto je za popunjavanje potreban poseban operator crtanja nakon što je putanja napravljena, zašto boje i širine linija cure iz jednog oblika u sledeći osim ako ih ne izolujete, i zašto isti kod za crtanje može završiti na potpuno različitim mestima nakon jedne transformacije koordinata. Ovo je pregled tog modela izvršavanja definisanog u standardu ISO 32000: operatori na koje nailazite kada otvorite tok sadržaja i pravila koja određuju šta se prikazuje na stranici.

Tok sadržaja je postfiksni bajt-kod

Tok sadržaja je ravan niz bajtova koji se sastoji od operanada praćenih operatorima. Operandi dolaze prvi, a operator koji ih troši dolazi na kraju, što je obrnuto od poziva funkcije i identično stek mašini: postavite brojeve, a zatim zadajte radnju (glagol). Nema ugnježdavanja, sintakse izraza, niti promenljivih. Kontura trougla se sastoji od pet linija koda:

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

Operatori su namerno kratki. Stvarna stranica sadrži hiljade ovakvih linija, obično komprimovanih pomoću FlateDecode filtra. Cena te kompaktnosti je to što tok ne nosi nikakvu strukturu koju možete upitati: čitač ne može pitati "gde je naslov na ovoj stranici", već može samo pokrenuti program i videti gde mastilo završava na papiru. To je osnovni razlog zašto je ekstrakcija teksta iz proizvoljnih PDF-ova teška.

Početak koordinatnog sistema je dole levo, a Y raste nagore

Pre nego što bilo koja koordinata dobije smisao, morate znati gde se nalazi tačka (0, 0). PDF postavlja koordinatni početak u donji levi ugao stranice, pri čemu se X povećava udesno, a Y raste nagore, mereno u tačkama (points) gde 72 tačke čine jedan inč (ISO 32000-2 §8.3.2). Na stranici formata US Letter, gornja ivica se nalazi na y = 792, a ne na y = 0. Svako ko dolazi iz sveta računarske grafike ekrana, gde je koordinatni početak gore levo, a Y raste nadole, iz prvog pokušaja ovo uradi naopako i nacrta prvu liniju ispod donje ivice stranice. Merna jedinica je takođe nezavisna od medijuma: 72 jedinice predstavlja jedan inč, bez obzira na to da li se stranica prikazuje na ekranu telefona ili na mašini za osvetljavanje filmova.

Većina biblioteka za crtanje stranica direktno nasleđuje ovu konvenciju. U HotPDF-u, na primer, TextOut i pozivi putanja mere se od donjeg levog ugla u tačkama, tako da vrednost blizu visine stranice postavlja sadržaj na vrh:

// 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

Taj redosled poziva se prevodi tačno u operatore m, l i S koji su navedeni iznad. Biblioteka je samo posrednik koji piše u tok sadržaja, i ništa više, a poznavanje onoga što ona emituje je ono što vam omogućava da razumete izlaz kada neki oblik završi tamo gde niste očekivali.

Napravite putanju, pa je nacrtajte

PDF razdvaja konstrukciju putanje od njenog iscrtavanja, i to razdvajanje nije cepodlačenje. Prvo opisujete oblik pomoću operatora konstrukcije koji ne dodaju ništa vidljivo, a zatim zadajete jedan operator crtanja koji odlučuje šta da uradi sa sakupljenom putanjom. Isti trougao može biti samo kontura, puno popunjavanje ili i jedno i drugo, u zavisnosti isključivo od operatora kojim završite.

Operatora konstrukcije ima malo. Operator m započinje novu podputanju u određenoj tački. Operator l dodaje pravolinijski segment. Operator c dodaje kubnu Bezierovu krivu iz šest operanada, dve kontrolne tačke i krajnje tačke. Operator re je prečica koja dodaje ceo pravougaonik na osnovu parametara x, y, širine i visine. Operator h zatvara trenutnu podputanju povezujući je nazad sa njenim početkom. Nijedan od njih ne ostavlja trag na stranici; oni samo prikupljaju geometriju.

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

Prvobitni primer je koristio sada zastarelu varijantu y operatora krive; c sa svoje tri eksplicitne tačke je oblik koji ćete videti u praksi i onaj koji treba koristiti. Kada putanja postoji, jedan operator crtanja je završava. Spisak je mali i vredi ga zapamtiti, jer se svaki oblik na svakoj stranici završava jednim od ovih operatora:

  • S crta konturu putanje koristeći trenutnu širinu linije i boju konture.
  • f popunjava unutrašnjost koristeći trenutnu boju popunjavanja i pravilo nenultog namotaja (nonzero winding rule).
  • f* popunjava koristeći pravilo parno-neparno (even-odd rule), što je važno za oblike koji se samopresecaju i oblike sa rupama.
  • B popunjava, a zatim crta konturu u jednoj operaciji; b prvo zatvara putanju.
  • n ne crta ništa, što je način na koji putanja postaje region za isecanje (clip region) bez ostavljanja vidljivog traga.

Pravilo namotaja je deo u kom ljudi najčešće greše. Pravilo nenultog namotaja (f, B) broji usmerene preseke zraka iz test tačke i popunjava svuda gde broj nije nula, tako da rupa ostaje prazna samo ako je smer njene podputanje suprotan od spoljne putanje. Pravilo parno-neparno (f*, B*) menja stanje pri svakom preseku bez obzira na smer. Ako oblik "krofne" ispadne potpuno popunjen, to znači da je unutrašnji krug nacrtan u istom smeru kao i spoljašnji, pa morate ili promeniti smer ili preći na pravilo parno-neparno.

Boja je režim rada, a ne parametar

Boja u toku sadržaja je postojana (sticky). Jednom kada postavite boju, ona ostaje aktivna sve dok ne postavite drugu ili ne vratite prethodno stanje, zbog čega neizolovana promena boje tiho boji sve što se nacrta nakon nje. PDF takođe drži boju popunjavanja i boju konture kao dva nezavisna podešavanja, koristeći mala slova za popunjavanje i velika slova za konturu. Prostori boja uređaja imaju svoje skraćene operatore:

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 pogodan za prikaz na ekranu, DeviceCMYK je ono što grafička priprema za štampu očekuje, dok je DeviceGray najmanji izbor za monohromatski sadržaj. Ovi prostori boja su praktični, ali nisu kalibrisani: ista RGB kombinacija može izgledati drugačije na dva različita monitora, što je problem koji rešavaju ICC prostori boja i PDF/A standardi. Za rad gde je boja kritična, birate kalibrisani prostor pomoću cs i CS, a komponente postavljate sa sc i scn, ali za obične dokumente skraćenice prostora boja obavljaju najveći deo posla. Biblioteka obavija ove operatore u tipizirane pozive. HotPDF, na primer, prima jednu vrednost TColor i emituje odgovarajuće operatore:

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;

Grafičko stanje i q/Q stek

Sve što nije sama putanja nalazi se u grafičkom stanju: trenutna matrica transformacije, boje popunjavanja i konture, širina linije, šablon isprekidanosti, region za isecanje, alfa kanal. Stanje je globalno i promenljivo, tako da je jedini siguran način za lokalnu izmenu da sačuvate kompletno stanje, izmenite ga, nacrtate šta je potrebno i zatim ga vratite na staro. To je ono što rade operatori q i Q. Operator q stavlja kopiju trenutnog stanja na stek, dok ga Q skida sa steka, odbacujući svaku izmenu napravljenu od poslednjeg poziva operatora 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

Neuravnoteženi operatori q i Q su čest razlog zašto ručno pravljeni ili spojeni tokovi sadržaja greše. Zalutali operator q bez odgovarajućeg Q ostavlja previše elemenata na steku kada se stranica završi, dok suvišan operator Q dovodi do podlivanja steka (underflow). U oba slučaja, čitač može zadržati stari region isecanja ili transformaciju, pa sadržaj nestaje ili završava na pogrešnom mestu. Kada grafika nestane bez razloga koji se može objasniti geometrijom putanje, prvo proverite stek grafičkih stanja.

CTM transformiše svaku koordinatu

Trenutna matrica transformacije (Current Transformation Matrix - CTM) nalazi se između brojeva u vašim operatorima i stvarne stranice. Svaka koordinata se množi sa CTM pre nego što se bilo šta nacrta, tako da promena matrice menja gde i kako se prikazuje svo naknadno crtanje, bez menjanja ijedne koordinate same putanje. Operator cm nadovezuje novu matricu na trenutnu, primajući šest operanada koji se mapiraju u afinu matricu [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 stvari često zbunjuju programere. Prvo, operator cm kombinuje matrice umesto da ih zamenjuje, tako da se transformacije akumuliraju i redosled je važan: skaliranje pa translacija nije isto što i translacija pa skaliranje. Drugo, rotacija i skaliranje se vrše oko trenutnog koordinatnog početka, a ne oko centra vašeg oblika, pa da biste rotirali objekat u mestu, morate ga translirati u koordinatni početak, rotirati, a zatim translirati nazad, i sve to obaviti unutar q/Q bloka. Ova ista matrica se koristi i za postavljanje slika, što je poslednji važan element koji treba analizirati.

Slike i višekratni sadržaj su XObject-i

Rasterske slike se ne nalaze direktno unutar toka sadržaja. One se čuvaju kao slikovni XObject-i (image XObjects), spoljni objekti sa sopstvenim rečnikom koji opisuje širinu, visinu, dubinu bita, prostor boja i filter kompresije, dok se tok sadržaja samo referencira na njih. Fotografija podržana JPEG formatom deklariše se na sledeći način:

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

Slikovni XObject se crta u jediničnom kvadratu: on uvek zauzima region od (0, 0) do (1, 1) u korisničkom prostoru. Ne prosleđujete mu poziciju niti veličinu. Umesto toga, podešavate CTM tako da se taj jedinični kvadrat mapira na željeni pravougaonik, a zatim ga pozivate pomoću operatora Do. Zato je postavljanje slike uvek transformacija praćena pozivom, umotana u save/restore blok kako se skaliranje ne bi prenelo na sledeću operaciju:

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

Isti mehanizam Do pokreće i form XObject-e, koji drže višekratne delove grafike, poput logotipa ili ponovljenih pečata, kao sopstveni tok sadržaja sa graničnim okvirom (bounding box). Definišete ga jednom, pozovete ga mnogo puta sa različitim CTM-om, a bajtovi se u datoteci pojavljuju samo jednom. Većina biblioteka krije ovaj proces iza jednog poziva za postavljanje: HotPDF registruje bitmapu pomoću AddImage i postavlja je sa ShowImage, primajući eksplicitne vrednosti za x, y, širinu i visinu umesto da od vas traži da ručno gradite matricu:

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;

Ispod te jedne linije koda, biblioteka piše rečnik slikovnog XObject-a, podešava CTM da bi dimenzionisala i pozicionirala jedinični kvadrat i emituje operator Do. Model koji leži u osnovi je onaj koji vredi znati, jer objašnjava svaki neobičan rezultat: rastegnuta slika je posledica neusklađenih faktora skaliranja u CTM-u, logotip koji je identičan na četrdeset stranica je jedan form XObject pozvan četrdeset puta, a slika koja se prikazuje naopako ukazuje na promenu znaka u matrici, a ne na oštećenu datoteku.

Kuda ovo vodi

Grafički model je jednostavan kada sagledate njegovu strukturu. Tok sadržaja je postfiksni bajt-kod koji se izvršava u odnosu na promenljivo stanje; koordinate kreću od donjeg levog ugla i prolaze kroz CTM; putanje se grade tiho i crtaju jednim određenim operatorom; podešavanja boje i linija opstaju sve dok ih ne izolujete pomoću q/Q; slike i višekratna grafika su XObject-i koji se postavljaju transformacijom jediničnog kvadrata. Skoro svaki zbunjujući rezultat iscrtavanja svodi se na jedno od tih pet pravila. Ako želite da vidite kako se ovi grafički operatori uklapaju u širi model objekata, rečnike stranica i tabelu unakrsnih referenci koje pokazuju na njih, tehnički pregled strukture PDF datoteke pokriva taj sloj, a kreiranje jednostavnog PDF-a ispočetka vodi vas kroz bajtove od početka do kraja. Crtanje teksta živi u sopstvenoj porodici operatora i ima svoje zamke, što je pokriveno u pratećem tekstu o rukovanju PDF tekstom i fontovima.

Delphi pozivi za crtanje koji su ovde prikazani, MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage i ShowImage, deo su HotPDF Component-e za Delphi i C++Builder, koja emituje ove operatore toka sadržaja umesto vas.