Teknisk artikkel

Hvordan PDF-grafikk fungerer: Innholdsstrømmer og operatører

En PDF-side lagrer ikke piksler, og den lagrer ikke et tre av formobjekter slik SVG gjør. Den lagrer et program. Hver linje, kurve, fylling og plasserte bilde på siden er resultatet av å utføre en sekvens med operatører i en innholdsstrøm, fra topp til bunn, mot en kjørende grafisk tilstand. Forstå det ene faktumet, og det meste av formatets oppførsel slutter å være overraskende: hvorfor en fylling trenger en separat maleoperatør etter at stien er bygget, hvorfor farger og linjebredder lekker fra én form til den neste med mindre du omslutter dem i parentes, hvorfor den samme tegnekoden kan havne på helt forskjellige steder etter en enkelt koordinattransformasjon. Dette er en rundtur i den utførelsesmodellen slik den er definert i ISO 32000: operatørene du møter når du åpner en innholdsstrøm, og reglene som bestemmer hva som dukker opp på siden

Innholdsstrømmen er suffiks (postfix) bytekode

En innholdsstrøm er en flat bytesekvens av operander etterfulgt av operatører. Operander kommer først, operatøren som konsumerer dem kommer til slutt, noe som er det motsatte av et funksjonskall og identisk med en stakkmaskin: skyv tallene (push), og utsted deretter verbet. Det er ingen nesting, ingen uttrykkssyntaks, ingen variabler. Omrisset av en trekant er fem linjer av dette:

100 100 m
200 100 l
150 200 l
h
S

Operatørene er knappe med vilje. En ekte side er tusenvis av disse, vanligvis komprimert med FlateDecode. Kostnaden for den kompaktheten er at strømmen ikke bærer noen struktur du kan spørre om: et visningsprogram kan ikke spørre "hvor er overskriften på denne siden," det kan bare kjøre programmet og se hva slags blekk som lander hvor. Det er hovedgrunnen til at tekstuttrekking fra vilkårlige PDF-er er vanskelig

Origo er nede til venstre, og Y vokser oppover

Før noen koordinat gir mening, må du vite hvor (0, 0) er. PDF setter origo i nedre venstre hjørne av siden, med X økende mot høyre og Y økende oppover, målt i punkter ved 72 punkter per tomme (ISO 32000-2 §8.3.2). På en US Letter-side sitter toppkanten på y = 792, ikke på y = 0. Alle som kommer fra skjermgrafikk, hvor origo er oppe til venstre og Y vokser nedover, får dette bakvendt på første forsøk og tegner den første linjen utenfor bunnen av siden. Enheten er også uavhengig av mediet: 72 enheter er én tomme, enten siden gjengis på en telefonskjerm eller en fotosetter

De fleste side-tegnebiblioteker arver denne konvensjonen direkte. I HotPDF, for eksempel, måler TextOut og stikallene alt fra bunn-venstre i punkter, så en verdi nær sidehøyden plasserer innhold øverst:

// Draws a triangle at the bottom of the page
Page.MoveTo(100, 100);
Page.LineTo(200, 100);
Page.LineTo(150, 200);
Page.ClosePath;
Page.Stroke;

Den kallsekvensen kompileres ned til nøyaktig m-, l- og S-operatørene ovenfor. Biblioteket er en maskinskriver for innholdsstrømmen, ingenting mer, og å vite hva den avgir er det som lar deg resonnere rundt utdataene når en form lander et sted du ikke forventet

Bygg stien, mal den deretter

PDF skiller stikonstruksjon fra stimale, og separasjonen er ikke pedanteri. Du beskriver først en form med konstruksjonsoperatører som ikke legger til noe synlig, og deretter utsteder du en enkelt maleoperatør som bestemmer hva som skal gjøres med den akkumulerte stien. Den samme trekanten kan være et omriss, et solid fyll, eller begge deler, bare avhengig av verbet du slutter med

Konstruksjonsoperatørene er få. m starter en ny understi ved et punkt. l legger til et rett segment. c legger til en kubisk Bezier-kurve fra seks operander, to kontrollpunkter og et endepunkt. re er en snarvei som legger til et helt rektangel fra en kvadruppel av x, y, bredde og høyde. h lukker gjeldende understi tilbake til dens start. Ingen av dem legger blekk på siden; de bare akkumulerer geometri

100 100 m
100 200 200 200 200 100 c
h

Det opprinnelige eksemplet brukte den nå foreldede y-varianten av kurveoperatøren; c med sine tre eksplisitte punkter er formen du vil se i praksis, og den du bør gripe til. Når stien eksisterer, fullfører én maleoperatør den. Vokabularet er lite og verdt å memorere, for hver form på hver side ender med en av disse:

  • S trekker opp stiomrisset med gjeldende linjebredde og strøkfarge
  • f fyller det indre ved å bruke gjeldende fyllfarge og "nonzero winding"-regelen
  • f* fyller ved å bruke like-odde-regelen (even-odd rule), noe som er viktig for selv-kryssende former og former med hull
  • B fyller og trekker opp strøk i én operasjon; b lukker stien først
  • n maler ingenting, noe som er hvordan en sti blir en beskjæringsregion (clip region) uten å etterlate et synlig merke

Viklingsregelen (winding rule) er den delen folk får feil. Nonzero (f, B) teller fortegnede krysninger av en stråle fra testpunktet og fyller overalt der tellingen ikke er null, så et hull forblir bare tomt hvis understien dens vikles motsatt av den ytre. Like-odde (f*, B*) bytter ved hver krysning uavhengig av retning. Hvis en "smultring"-form kommer ut massiv, vikles den indre sirkelen samme vei som den ytre, og du enten reverserer den eller bytter til like-odde

Farge er en modus, ikke en parameter

Farge i en innholdsstrøm er klissete. Du angir en farge, og den forblir satt inntil du angir en annen eller gjenoppretter en tidligere tilstand, noe som er grunnen til at en endring av farge uten parenteser i stillhet farger alt som tegnes etter den. PDF beholder også fyllfarge og strøkfarge som to uavhengige innstillinger, med små bokstaver for fyll og store bokstaver for strøk. Enhetsfargerommene har hver sin snarvei:

0.1 0.2 0.8 rg    % set RGB fill color
0.9 0.1 0.1 RG    % set RGB stroke color
0.5 g             % set grayscale fill color
0 0 0 1 K         % set CMYK stroke color (100% black)

DeviceRGB passer til skjermvisning, DeviceCMYK er hva trykkproduksjon forventer, og DeviceGray er det minste valget for monokromt innhold. Enhetsrommene er praktiske, men ukalibrerte: den samme RGB-trippelen kan gjengis forskjellig på to skjermer, noe som er problemet ICC-baserte fargerom og PDF/A output intents eksisterer for å løse. For fargekritisk arbeid velger du et kalibrert rom med cs og CS og angir komponenter med sc og scn, men for vanlige dokumenter tar enhetssnarveiene byrden. Et bibliotek pakker disse inn i typede kall. HotPDF, for eksempel, tar en enkelt TColor og utsteder de matchende operatørene:

Page.SetRGBFillColor(clBlue);   // emits RGB fill operators
Page.SetRGBStrokeColor(clRed);  // emits RGB stroke operators

Den grafiske tilstanden og q/Q-stakken

Alt som ikke er selve stien, lever i den grafiske tilstanden: gjeldende transformasjonsmatrise, fyll- og strøkfarger, linjebredde, stiplet mønster, beskjæringsregion, alfa. Tilstanden er global og foranderlig, så den eneste trygge måten å gjøre en lokal endring på er å lagre det hele, endre det, tegne og rulle det tilbake. Det er det q og Q gjør. q skyver en kopi av gjeldende tilstand over på en stakk; Q popper den (henter den), og forkaster hver endring gjort siden den matchende q-en

q               % save state
2 w             % set line width to 2
1 0 0 RG        % set stroke to red
100 100 m
200 200 l
S               % draw a thick red line
Q               % restore state: line width and color return to previous values

Ubalanserte q og Q er en vanlig måte en håndbygd eller sydd innholdsstrøm går feil på. En bortkommen q uten matchende Q etterlater stakken dyp når siden slutter; en ekstra Q underflyter (underflows) den. Uansett kan et visningsprogram holde på et gammelt kutt eller transformasjon, og innhold forsvinner eller lander på feil sted. Når grafikk forsvinner uten noen grunn stien kan forklare, sjekk tilstandsstakken først

CTM transformerer hver koordinat

Gjeldende transformasjonsmatrise (CTM) sitter mellom tallene i operatørene dine og den faktiske siden. Hver koordinat multipliseres med CTM-en før noe tegnes, så å endre matrisen endrer hvor og hvordan alle påfølgende tegninger vises uten å røre en eneste stikoordinat. cm-operatøren sammenkjeder en ny matrise med den gjeldende, og tar seks operander som tilordnes den affine matrisen [a b c d e f]:

q
0.866 0.5 -0.5 0.866 100 100 cm   % translate to 100,100 and rotate 30 degrees
0 0 m
100 0 l
S                                 % line draws rotated, starting at 100,100
Q

To ting får folk til å snuble. For det første komponerer cm snarere enn å erstatte, så transformasjoner akkumuleres og rekkefølge betyr noe: å skalere og deretter forskyve er ikke det samme som å forskyve og deretter skalere. For det andre, roterer og skalerer man rundt gjeldende origo, ikke midten av formen din, så for å rotere noe på plass forskyver du det til origo, roterer det, og forskyver det deretter tilbake, alt pakket inn i q/Q. Denne samme matrisen er det som plasserer bilder, den siste biten det er verdt å se på

Bilder og gjenbrukbart innhold er XObjects

Rasterbilder lever ikke "inline" i innholdsstrømmen. De lagres som bilde-XObjects, eksterne objekter med sin egen ordbok som beskriver bredde, høyde, bitdybde, fargerom og kompresjonsfilter, og innholdsstrømmen bare refererer til dem. Et JPEG-støttet foto deklarerer seg selv slik:

12 0 obj
<< /Type /XObject
   /Subtype /Image
   /Width 800
   /Height 600
   /ColorSpace /DeviceRGB
   /BitsPerComponent 8
   /Filter /DCTDecode
   /Length 45200 >>
stream
... jpeg bytes ...
endstream
endobj

Et bilde-XObject tegnes inn i enhetskvadratet: det opptar alltid regionen fra (0, 0) til (1, 1) i brukerområdet. Du gir det verken en posisjon eller størrelse. I stedet angir du CTM-en slik at enhetskvadratet tilordnes rektangelet du vil ha, og kaller det deretter opp med Do. Det er grunnen til at å plassere et bilde alltid er en transformasjon etterfulgt av en påkalling, pakket inn i en lagre/gjenopprett (save/restore) slik at skalaen ikke blør inn i den neste operasjonen:

q
400 0 0 300 50 100 cm   % scale to 400x300 and place at x=50, y=100
/Im1 Do                 % invoke the image XObject mapped to /Im1
Q

Den samme Do-mekanismen driver form-XObjects, som inneholder en gjenbrukbar bit med grafikk, en logo eller et gjentatt stempel, som sin egen innholdsstrøm med en avgrensende boks. Definer den én gang, påkall den mange ganger med ulik CTM, og bytene dukker bare opp i filen én gang. De fleste biblioteker skjuler dette bak et enkelt plasseringskall: HotPDF registrerer et bitmap med AddImage og plasserer det med ShowImage, som tar en eksplisitt x, y, bredde og høyde, i stedet for å be deg bygge matrisen for hånd:

Pdf.AddImage('logo.jpg', 'Im1');
Page.ShowImage('Im1', 50, 100, 400, 300);

Under den ene linjen skriver biblioteket bilde-XObject-ordboken, angir CTM-en til å dimensjonere og plassere enhetskvadratet, og utsteder Do. Modellen under er den som er verdt å kjenne til, for den forklarer alle merkelige resultater: et strukket bilde er en CTM med ikke-overensstemmende skaleringsfaktorer, en logo identisk på førti sider er ett form-XObject påkalt førti ganger, og et bilde som gjengis opp ned, er en fortegnsflipp i matrisen, ikke en ødelagt fil

Hvor dette fører hen

Grafikkmodellen er liten når du først ser formen på den. En innholdsstrøm er suffiks (postfix) bytekode som kjører mot en foranderlig tilstand; koordinater starter nede til venstre og passerer gjennom CTM; stier bygges i stillhet og males med én overlagt operatør; farge- og linjeinnstillinger vedvarer til du omgir dem med q/Q; bilder og gjenbrukbar grafikk er XObjects plassert ved å transformere et enhetskvadrat. Nesten hvert forvirrende gjengivelsesresultat reduseres til en av disse fem reglene. Hvis du vil se hvordan disse grafiske operatørene sitter i den større objektmodellen, sideordbøkene og kryssreferansetabellen som peker på dem, dekker den tekniske oversikten over PDF-filstruktur det laget, og bygging av en enkel PDF fra bunnen av går gjennom bytene fra ende til ende. Teksttegning lever i sin egen operatørfamilie og har sine egne fallgruver, dekket i den tilhørende artikkelen om håndtering av PDF-tekst og skrifttype

Delphi-tegnekallene vist her, MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage og ShowImage, er en del av HotPDF Component for Delphi og C++Builder, som utsteder disse innholdsstrøm-operatørene for deg