Techninis straipsnis

Kaip veikia PDF grafika: turinio srautai ir operatoriai

PDF puslapyje nesaugomi pikseliai ir nesaugomas figūrų objektų medis, kaip yra SVG atveju. Jame saugoma programa. Kiekviena puslapio linija, kreivė, užpildas ir įterptas vaizdas yra operatorių sekos vykdymo turinio sraute (iš viršaus į apačią) rezultatas, atsižvelgiant į veikiančią grafikos būseną. Supratus šį vieną faktą, didžioji dalis šio formato elgsenos nustoja stebinti: kodėl užpildui reikalingas atskiras piešimo operatorius po to, kai sukuriamas kelias, kodėl spalvos ir linijų storiai „nuteka“ iš vienos figūros į kitą, jei jų neapribojate, ir kodėl tas pats braižymo kodas po vienos koordinačių transformacijos gali atsidurti visiškai kitoje vietoje. Tai yra trumpa apžvalga apie šį vykdymo modelį, apibrėžtą standartu ISO 32000: operatoriai, su kuriais susiduriate atvėrę turinio srautą, ir taisyklės, lemiančios, kas bus rodoma puslapyje

Turinio srautas yra postfiksinis baitinis kodas

Turinio srautas yra plokščia baitų seka, kurią sudaro operandai, o po jų seka operatoriai. Pirmiausia pateikiami operandai, o juos apdorojantis operatorius eina gale – tai yra priešinga tvarka nei funkcijos iškvietimas ir visiškai atitinka stako (dėklo) mašinos principą: įkeliami skaičiai, o tada vykdomas veiksmas. Čia nėra įdėjimo (nesting), reiškinių sintaksės ar kintamųjų. Trikampio kontūras susideda iš penkių tokių eilučių:

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

Operatoriai yra sąmoningai glausti. Tikrame puslapyje jų būna tūkstančiai, paprastai suspausti naudojant „FlateDecode“. Tokio kompaktiškumo kaina yra ta, kad sraute nėra struktūros, kurios būtų galima užklausti: peržiūros programa negali paklausti „kur šiame puslapyje yra antraštė“, ji gali tik paleistinėti programą ir pamatyti, kur atsiduria rašalas. Tai yra pagrindinė priežastis, kodėl teksto išgavimas iš bet kokių PDF dokumentų yra sudėtingas uždavinys

Pradžios taškas yra apačioje kairėje, o Y vertė auga į viršų

Prieš pradedant naudoti koordinates, būtina žinoti, kur yra taškas (0, 0). PDF formatu pradžios taškas nustatomas apatiniame kairiajame puslapio kampe, kur X didėja į dešinę, o Y didėja į viršų, matuojant taškais (72 taškai colyje, ISO 32000-2 §8.3.2). Pavyzdžiui, JAV standartinio „Letter“ formato puslapio viršutinis kraštas yra ties y = 792, o ne ties y = 0. Visi, atėję iš kompiuterio ekranų grafikos pasaulio, kur pradžios taškas yra viršuje kairėje, o Y didėja žemyn, iš pradžių daro klaidą ir nubrėžia pirmąją liniją žemiau puslapio apačios. Be to, šis matavimo vienetas nepriklauso nuo laikmenos: 72 vienetai yra vienas colis, nesvarbu, ar puslapis atvaizduojamas telefono ekrane, ar vaizdo ryškinimo įrenginyje

Dauguma puslapių braižymo bibliotekų tiesiogiai perima šią taisyklę. Pavyzdžiui, „HotPDF“ bibliotekoje funkcijos TextOut ir visi kelio iškvietimai matuojami nuo apatinio kairiojo kampo taškais, todėl reikšmė, artima puslapio aukščiui, patalpina turinį viršuje:

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

Ši iškvietimų seka sukompiliuojama būtent į minėtus m, l ir S operatorius. Biblioteka yra tik turinio srauto generatorius, nieko daugiau, o žinojimas, ką ji sukuria, leidžia suprasti rezultatą, kai figūra atsiduria ne ten, kur tikėjotės

Sukurkite kelią, tada jį nupieškite

PDF atskiria kelio kūrimą nuo kelio piešimo, ir šis atskyrimas nėra tik pedantiškumas. Pirmiausia aprašote figūrą kūrimo operatoriais, kurie nesukuria nieko matomo, o tada iškviečiate vieną piešimo operatorių, kuris nusprendžia, ką daryti su sukauptu keliu. Tas pats trikampis gali būti tik kontūras, vientisas užpildas arba abu kartu, priklausomai tik nuo to, kokį veiksmą pasirenkate pabaigoje

Kūrimo operatorių yra nedaug. Operatorius m pradeda naują kelio dalį taške. Operatorius l prideda tiesią atkarpą. Operatorius c prideda kubinę Bezjė (Bezier) kreivę iš šešių operandų (dviejų valdymo taškų ir pabaigos taško). Operatorius re yra trumpinys, pridedantis visą stačiakampį pagal keturis parametrus: x, y, plotį ir aukštį. Operatorius h uždaro esamą kelio dalį sujungdamas ją su pradžios tašku. Nė vienas iš jų nepiešia puslapyje; jie tik kaupia geometrinius duomenis

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

Pradiniame pavyzdyje buvo naudojamas dabar jau nenaudojamas kreivės operatoriaus variantas y; praktikoje dažniausiai sutinkamas ir naudojamas operatorius c su trimis aiškiai nurodytais taškais. Kai kelias sukurtas, jį užbaigia vienas piešimo operatorius. Šis žodynas yra nedidelis ir jį verta įsiminti, nes kiekviena figūra kiekviename puslapyje baigiasi vienu iš šių operatorių:

  • S apibrėžia kelio kontūrą, naudodamas esamą linijos plotį ir kontūro spalvą
  • f užpildo vidų, naudodamas esamą užpildo spalvą ir nelyginio apėjimo taisyklę (nonzero winding rule)
  • f* užpildo vidų naudodamas lyginio-nelyginio (even-odd) taisyklę, kuri yra svarbi susikertančioms figūroms arba figūroms su skylėmis
  • B užpildo ir tada apibrėžia kontūrą viena operacija; b prieš tai uždaro kelią
  • n nieko nepiešia – taip kelias paverčiamas apkarpymo sritimi (clip region) nepaliekant matomos žymės

Apėjimo taisyklė yra ta dalis, kurioje žmonės dažniausiai klysta. Nelyginio apėjimo (Nonzero) taisyklė (f, B) skaičiuoja ženklo krypties susikirtimus su spinduliu iš tikrinamo taško ir užpildo visur, kur skaičius nėra nulis, todėl skylė lieka tuščia tik tuo atveju, jei jos vidinis kelias eina priešinga kryptimi nei išorinis. Lyginio-nelyginio (Even-odd) taisyklė (f*, B*) persijungia kiekviename susikirtimo taške, nepriklausomai nuo krypties. Jei spurgos formos figūra užsipildo visa, vadinasi, vidinis apskritimas yra nubrėžtas ta pačia kryptimi kaip ir išorinis, todėl turite jį pakeisti priešingu arba perjungti į lyginio-nelyginio taisyklę

Spalva yra režimas, o ne parametras

Spalva turinio sraute išlieka aktyvi (yra „lipni“). Nustačius spalvą, ji išlieka tokia pati, kol nenustatote kitos arba nesugrąžinate ankstesnės būsenos. Štai kodėl neapribotas spalvos pakeitimas nepastebimai nuspalvina viską, kas braižoma po jo. PDF formatas taip pat laiko užpildo spalvą ir kontūro spalvą kaip du nepriklausomus nustatymus: mažosios raidės skirtos užpildo operatoriams, o didžiosios – kontūro operatoriams. Kiekviena įrenginio spalvų erdvė turi savo trumpinius:

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“ tinka ekrano išvesčiai, „DeviceCMYK“ tikimasi spaudos gamyboje, o „DeviceGray“ yra mažiausias pasirinkimas vienspalviam turiniui. Įrenginio spalvų erdvės yra patogios, bet nekalibruotos: tas pats RGB trejetas skirtinguose monitoriuose gali atrodyti skirtingai – būtent šią problemą sprendžia ICC pagrindu sukurtos spalvų erdvės ir PDF/A išvesties nustatymai (output intents). Tiksliam darbui su spalvomis pasirenkama kalibruota erdvė su operatoriais cs ir CS, o komponentai nustatomi su sc ir scn, tačiau įprastiems dokumentams pakanka įrenginio trumpinių. Biblioteka šiuos iškvietimus apgaubia tipizuotomis funkcijomis. Pavyzdžiui, „HotPDF“ priima vieną TColor reikšmę ir sugeneruoja atitinkamus operatorius:

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;

Grafikos būsena ir q/Q dėklas

Viskas, kas nėra pats kelias, saugoma grafikos būsenoje: dabartinė transformacijos matrica, užpildo ir kontūro spalvos, linijos plotis, brūkšninė linija (dash pattern), apkarpymo sritis ir alfa kanalas. Būsena yra globali ir keičiama, todėl vienintelis saugus būdas atlikti vietinį pakeitimą yra išsaugoti ją visą, pakeisti reikšmes, nupiešti objektą ir sugrąžinti pradinę būseną. Būtent tai atlieka operatoriai q ir Q. Operatorius q įkelia dabartinės būsenos kopiją į dėklą (staką), o Q ją išima, atmesdamas visus pakeitimus, padarytus po atitinkamo 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

Nesubalansuoti q and Q operatoriai yra dažna priežastis, kodėl rankiniu būdu kuriamas arba sujungiamas turinio srautas sugadinamas. Paliktas papildomas q be atitinkamo Q lemia tai, kad puslapio pabaigoje dėklas lieka per gilus, o papildomas Q sukelia dėklo perpildymą (underflow). Bet kuriuo atveju peržiūros programa gali išlaikyti seną apkarpymą ar transformaciją, todėl turinys išnyksta arba atsiduria netinkamoje vietoje. Kai grafika dingsta be jokios aiškios priežasties, pirmiausia patikrinkite būsenos dėklą

CTM transformuoja kiekvieną koordinatę

Dabartinė transformacijos matrica (CTM – Current Transformation Matrix) yra tarp jūsų operatorių skaičių ir tikrojo puslapio. Kiekviena koordinatė prieš braižymą yra padauginama iš CTM, todėl pakeitus matricą pasikeičia visi vėlesni braižymo elementai ir jų vieta, neliečiant nė vienos kelio koordinatės. Operatorius cm prijungia naują matricą prie esamos, priimdamas šešis operandus, atitinkančius afininę matricą [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)

Žmonės dažniausiai susiduria su dviem problemomis. Pirma, operatorius cm ne pakeičia, o sujungia matricas, todėl transformacijos kaupiasi ir jų eiliškumas yra svarbus: mastelio keitimas ir po to sekantis poslinkis duoda kitokį rezultatą nei poslinkis, po kurio keičiamas mastelis. Antra, pasukimas ir mastelio keitimas vyksta aplink dabartinį pradžios tašką, o ne aplink jūsų figūros centrą, todėl norėdami pasukti objektą vietoje, turite jį pastumti į pradžios tašką, pasukti ir pastumti atgal – viską apgaubiant q/Q. Ta pati matrica naudojama ir vaizdams patalpinti, o tai yra paskutinė svarbi dalis

Vaizdai ir pakartotinai naudojamas turinys yra XObject objektai

Rastriniai vaizdai nėra saugomi tiesiogiai turinio srauto viduje. Jie yra saugomi kaip vaizdo XObject objektai (išoriniai objektai su savo žodynu, aprašančiu plotį, aukštį, bitų gylį, spalvų erdvę ir suspaudimo filtrą), o turinio sraute pateikiama tik nuoroda į juos. Nuotrauka, kurios pagrindas yra JPEG, deklaruojama taip:

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

Vaizdo XObject objektas piešiamas vienetiniame kvadrate: vartotojo erdvėje jis visada užima sritį nuo (0, 0) to (1, 1). Jūs neperduodate jam padėties ar dydžio. Vietoj to nustatote CTM taip, kad vienetinis kvadratas būtų atvaizduotas į norimą stačiakampį, o tada iškviečiate jį naudodami operatorių Do. Štai kodėl vaizdo talpinimas visada susideda iš transformacijos ir vėlesnio iškvietimo, kurie yra apgaubti išsaugojimo / atkūrimo operatoriais, kad mastelis nepersiduotų kitai operacijai:

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

Tas pats operatoriaus Do mechanizmas valdo ir formų XObject objektus (form XObjects), kuriuose saugomas pakartotinai naudojamas grafikos elementas, pavyzdžiui, logotipas ar pasikartojantis spaudas, kaip atskiras turinio srautas su apribojimo rėmeliu (bounding box). Apibrėžkite jį vieną kartą, iškvieskite daug kartų su skirtinga CTM, ir failo baitai bus panaudoti tik vieną kartą. Dauguma bibliotekų tai paslepia po vienu talpinimo iškvietimu: „HotPDF“ užregistruoja bitų žemėlapį naudodama AddImage ir patalpina jį naudodama ShowImage, priimdama nurodytus x, y, plotį ir aukštį, užuot reikalavusi rankiniu būdu kurti transformacijos matricą:

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;

Po šia viena eilute biblioteka įrašo vaizdo XObject žodyną, nustato CTM, kad pakeistų vienetinio kvadrato dydį bei padėtį, ir sugeneruoja Do. Verta suprasti šį pagrindinį modelį, nes jis paaiškina bet kokį keistą rezultatą: ištęstas vaizdas reiškia netinkamus CTM mastelio koeficientus, identiškas logotipas keturiasdešimtyje puslapių yra vienas formos XObject, iškviestas keturiasdešimt kartų, o apverstas vaizdas rodo ženklo pasikeitimą matricoje, o ne sugadintą failą

Kur tai veda

Grafikos modelis yra paprastas, kai suprantate jo principą. Turinio srautas yra postfiksinis baitinis kodas, vykdomas keičiamoje būsenoje; koordinatės prasideda apatiniame kairiajame kampe ir eina per CTM; keliai kuriami nematomi ir nupiešiami vienu konkrečiu operatoriumi; spalvų ir linijų nustatymai išlieka, kol jų neapribojate su q/Q; vaizdai ir pakartotinai naudojama grafika yra XObject objektai, talpinami transformuojant vienetinį kvadratą. Beveik visi keistumai atvaizduojant turinį kyla dėl vienos iš šių penkių taisyklių. Jei norite pamatyti, kaip šie grafikos operatoriai dera platesniame objektų modelyje, puslapių žodynuose ir kryžminių nuorodų lentelėje (cross-reference table), techninė PDF failų struktūros apžvalga aprašo šį lygmenį, o straipsnis apie paprasto PDF dokumento kūrimą nuo nulio parodo baitų seką. Teksto rašymas priklauso kitai operatorių šeimai ir turi savų niuansų, kurie aptariami susijusiame straipsnyje apie PDF teksto ir šriftų tvarkymą

Čia parodyti „Delphi“ braižymo iškvietimai (MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage ir ShowImage) yra dalis „Delphi“ ir „C++Builder“ skirtos „HotPDF“ komponento, kuris sugeneruoja šiuos turinio srauto operatorius už jus