Teknisk artikkel

Slik virker PDF-grafikk: innholdsstrømmer og operatorer

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 hvert plassert bilde på siden er resultatet av at en sekvens av operatorer i en innholdsstrøm kjøres, ovenfra og ned, mot en løpende grafikktilstand. Forstå det ene faktumet, så slutter det meste av formatets oppførsel å være overraskende: hvorfor en fylling trenger en egen maleoperator etter at banen er bygget, hvorfor farger og linjebredder lekker fra én form over i den neste med mindre du klammer dem inn, hvorfor den samme tegnekoden kan lande helt andre steder etter én enkelt koordinattransform. Dette er en omvisning i den utførelsesmodellen slik den er definert i ISO 32000: operatorene du møter når du åpner en innholdsstrøm, og reglene som avgjør hva som dukker opp på siden

Innholdsstrømmen er postfiks bytekode

En innholdsstrøm er en flat bytesekvens av operander etterfulgt av operatorer. Operandene kommer først, operatoren som forbruker dem kommer sist, som er omvendt av et funksjonskall og identisk med en stakkmaskin: legg tallene på stakken, og gi så verbet. Det finnes ingen nesting, ingen uttrykkssyntaks, ingen variabler. En trekantomriss er fem linjer av dette:

100 100 m    % moveto: start en ny delbane i (100, 100)
200 200 l    % lineto: legg til et segment til (200, 200)
300 100 l    % lineto: legg til et segment til (300, 100)
h            % closepath: koble tilbake til starten
S            % stroke: mal baneomrisset

Operatorene er knappe med vilje. En virkelig side er tusenvis av dem, som regel komprimert med FlateDecode. Kostnaden ved den kompaktheten er at strømmen ikke bærer noen struktur du kan spørre mot: en viser kan ikke spørre «hvor er overskriften på denne siden», den kan bare kjøre programmet og se hvor blekket lander. Det er den grunnleggende årsaken til at tekstuttrekk fra vilkårlige PDF-er er vanskelig

Origo ligger nede til venstre, og Y vokser oppover

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

PDF-diagram over PDF-koordinatsystemet med origo nede til venstre og Y som vokser oppover, satt opp mot skjermkonvensjonen med origo øverst til venstre som tegner identiske koordinater nær bunnen av siden
PDF måler fra nedre venstre hjørne med Y økende oppover, som er grunnen til at skjermvaner parkerer din første tegning nederst på siden

De fleste sidetegningsbiblioteker arver denne konvensjonen direkte. I HotPDF måler for eksempel TextOut og banekallene alle fra nedre venstre hjørne i punkter, så en verdi nær sidehøyden legger innhold øverst:

// HotPDF, Delphi: y måles fra underkanten og oppover, i punkter
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700);   // nær toppen av siden
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke;             // skriver ut moveto/lineto/stroke-operatorene

Den kallsekvensen kompileres ned til nøyaktig m-, l- og S-operatorene ovenfor. Biblioteket er en maskinskriver for innholdsstrømmen, ikke noe mer, og å vite hva det skriver ut, er det som lar deg resonnere om utdataene når en form lander et sted du ikke ventet

Bygg banen, mal den så

PDF skiller banekonstruksjon fra banemaling, og skillet er ikke pedanteri. Du beskriver først en form med konstruksjonsoperatorer som ikke legger til noe synlig, og gir så én enkelt maleoperator som avgjør hva som skal gjøres med den oppsamlede banen. Den samme trekanten kan bli et omriss, en heldekkende fylling eller begge deler, avhengig bare av verbet du avslutter med

Konstruksjonsoperatorene er få. m starter en ny delbane i et punkt. l legger til et rett segment. c legger til en kubisk Bezier-kurve ut fra seks operander, to kontrollpunkter og et endepunkt. re er en snarvei som legger til et helt rektangel ut fra en firling av x, y, bredde og høyde. h lukker den gjeldende delbanen tilbake til starten. Ingen av dem legger blekk på siden; de samler bare geometri

200 250 m                    % start delbanen
300 350 400 450 500 250 c    % kubisk Bezier: to kontrollpunkter, så endepunkt
150 200 re                   % et rektangel på 150 x 200, lagt til som egen delbane
h                            % lukk

Det opprinnelige eksempelet brukte den nå foreldede y-varianten av kurveoperatoren; c med sine tre eksplisitte punkter er formen du vil se i praksis og den du bør gripe til. Når banen først finnes, fullfører én maleoperator den. Vokabularet er lite og verdt å pugge, fordi hver eneste form på hver eneste side ender med en av disse:

  • S streker opp baneomrisset med gjeldende linjebredde og strekfarge
  • f fyller innsiden med gjeldende fyllfarge og ikke-null-omslutningsregelen
  • f* fyller med partall-oddetallsregelen, som betyr noe for selvkryssende former og former med hull
  • B fyller og streker så opp i én operasjon; b lukker banen først
  • n maler ingenting, som er slik en bane blir et klippeområde uten å legge igjen et synlig merke
PDF-diagram over PDF-banemodellen der konstruksjonsoperatorene m, l, c, re og h samler usynlig geometri og én avsluttende operator S, f, f*, B eller n avgjør strek, fylling, fylling pluss strek, eller klipp
Baneoperatorer samler bare geometri, og det ene verbet som lukker banen avgjør om du får et omriss, en fylling, begge deler, eller et klipp

Omslutningsregelen er den delen folk tar feil av. Ikke-null (f, B) teller de fortegnsatte kryssingene til en stråle fra testpunktet og fyller overalt der tellingen ikke er null, så et hull forblir bare tomt hvis delbanen dens går motsatt vei av den ytre. Partall-oddetall (f*, B*) veksler ved hver kryssing uansett retning. Kommer en «smultring»-form ut heldekkende, går den indre sirkelen samme vei som den ytre, og da må du enten snu den eller bytte til partall-oddetall

Farge er en modus, ikke en parameter

Farge i en innholdsstrøm er klebrig. Du setter en farge, og den blir stående til du setter en annen eller gjenoppretter en tidligere tilstand, som er grunnen til at en uinnklammet fargeendring stille toner alt som tegnes etter den. PDF holder også fyllfarge og strekfarge som to uavhengige innstillinger, med små bokstaver som operatorer for fyll og store for strek. Enhetsfargerommene har hver sin forkortelse:

0.5 g                % DeviceGray-fyll, middels grå (0 = svart, 1 = hvit)
0.2 0.6 0.8 rg       % DeviceRGB-fyll
0.8 0.2 0.1 RG       % DeviceRGB-strek (store bokstaver = strek)
0.2 0.8 0.0 0.1 k    % DeviceCMYK-fyll

DeviceRGB passer for skjermutdata, DeviceCMYK er det trykkproduksjon forventer, og DeviceGray er det minste valget for monokromt innhold. Enhetsrommene er praktiske, men ukalibrerte: den samme RGB-trippelen kan gjengis ulikt på to skjermer, som er problemet ICC-baserte fargerom og PDF/A-utdataintensjoner finnes for å løse. For fargekritisk arbeid velger du et kalibrert rom med cs og CS og setter komponentene med sc og scn, men for vanlige dokumenter bærer enhetsforkortelsene lasset. Et bibliotek pakker disse inn i typede kall. HotPDF tar for eksempel én enkelt TColor og skriver ut de tilsvarende operatorene:

Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150);  // x, y, bredde, høyde
Pdf.CurrentPage.Fill;

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

Grafikktilstanden og q/Q-stakken

Alt som ikke er selve banen, bor i grafikktilstanden: gjeldende transformasjonsmatrise, fyll- og strekfarger, linjebredde, strekmønster, klippeområde, alfa. Tilstanden er global og foranderlig, så den eneste trygge måten å gjøre en lokal endring på er å lagre hele greia, endre den, tegne og rulle den tilbake. Det er det q og Q gjør. q legger en kopi av gjeldende tilstand på en stakk; Q henter den av igjen og forkaster hver endring som er gjort siden den tilhørende q

q                    % lagre hele grafikktilstanden
2 0 0 2 100 100 cm   % sett sammen en transform: skaler 2x, flytt til (100,100)
0.8 g                % grå fyll, avgrenset til denne blokken
% ... tegn skalert, grått innhold ...
Q                    % gjenopprett: transform og farge går tilbake

Ubalanserte q og Q er en vanlig måte en håndbygget eller sammensydd innholdsstrøm går galt på. En løs q uten tilhørende Q etterlater stakken dyp når siden slutter; en ekstra Q tømmer den for langt. Uansett kan en viser holde et gammelt klipp eller en gammel transform i kraft, og innhold forsvinner eller lander på feil sted. Når grafikk forsvinner uten at banen kan forklare det, gransk tilstandsstakken først

CTM-en transformerer hver koordinat

Den gjeldende transformasjonsmatrisen sitter mellom tallene i operatorene dine og selve siden. Hver koordinat multipliseres med CTM-en før noe tegnes, så å endre matrisen endrer hvor og hvordan all påfølgende tegning dukker opp, uten å røre en eneste banekoordinat. cm-operatoren setter en ny matrise sammen med den gjeldende, og tar seks operander som avbildes på den affine matrisen [a b c d e f]:

1 0 0 1 100 50 cm        % flytt med (100, 50): e og f bærer forskyvningen
2 0 0 1.5 0 0 cm         % skaler x med 2, y med 1,5: a og d er skalafaktorene
0.707 0.707 -0.707 0.707 0 0 cm   % roter 45 grader (cos/sin i a, b, c, d)

To ting snubler folk i. For det første setter cm sammen i stedet for å erstatte, så transformer hoper seg opp og rekkefølgen betyr noe: å skalere og så flytte er ikke det samme som å flytte og så skalere. For det andre dreier rotasjon og skalering rundt gjeldende origo, ikke rundt sentrum av formen din, så for å rotere noe på stedet flytter du det til origo, roterer, og flytter så tilbake, alt sammen pakket inn i q/Q. Denne samme matrisen er det som plasserer bilder, den siste biten som er verdt å se

Bilder og gjenbrukbart innhold er XObjekter

Rasterbilder bor ikke inne i innholdsstrømmen. De lagres som bilde-XObjekter, eksterne objekter med sin egen dictionary som beskriver bredde, høyde, bitdybde, fargerom og kompresjonsfilter, og innholdsstrømmen refererer bare til dem. Et JPEG-basert fotografi erklærer seg selv slik:

/Photo <<
  /Type /XObject
  /Subtype /Image
  /Width 640
  /Height 480
  /BitsPerComponent 8
  /ColorSpace /DeviceRGB
  /Filter /DCTDecode        % bildedataene er en JPEG-strøm
>>

Et bilde-XObjekt tegner inn i enhetskvadratet: det opptar alltid området fra (0, 0) til (1, 1) i brukerrommet. Du gir det ingen posisjon eller størrelse. I stedet setter du CTM-en slik at det enhetskvadratet avbildes på rektangelet du vil ha, og påkaller det så med Do. Det er grunnen til at det å plassere et bilde alltid er en transform etterfulgt av en påkalling, pakket inn i en lagring og gjenoppretting slik at skaleringen ikke lekker over i neste operasjon:

PDF-diagram over plassering av bilde-XObjekt der dictionaryen definerer piksler utenfor innholdsstrømmen, Do maler inn i enhetskvadratet, og cm avbilder det på målrektangelet pakket inn i q og Q
Strømmen lagrer ingen piksler — den påkaller et XObjekt inn i enhetskvadratet, og cm avbilder det kvadratet på målrektangelet
q
640 0 0 480 50 300 cm    % avbild enhetskvadratet på en 640x480-boks i (50, 300)
/Photo Do                % mal bilde-XObjektet
Q

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

var
  Bmp: TBitmap;
  ImgIndex: Integer;
begin
  Bmp := TBitmap.Create;
  try
    Bmp.LoadFromFile('logo.bmp');
    ImgIndex := Pdf.AddImage(Bmp, icFlate);
    // x, y (nedre venstre), bredde, høyde, rotasjonsvinkel
    Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
  finally
    Bmp.Free;
  end;
end;

Under den ene linjen skriver biblioteket bilde-XObjektets dictionary, setter CTM-en for å dimensjonere og plassere enhetskvadratet, og skriver ut Do. Modellen under er den som er verdt å kjenne, fordi den forklarer hvert eneste rare resultat: et strukket bilde er en CTM med ulike skalafaktorer, en logo som er identisk på førti sider er ett form-XObjekt påkalt førti ganger, og et bilde som gjengis opp ned er et fortegnsbytte i matrisen, ikke en ødelagt fil

Hvor dette fører

Grafikkmodellen er liten når du først ser formen på den. En innholdsstrøm er postfiks bytekode som kjører mot en foranderlig tilstand; koordinatene starter nede til venstre og går gjennom CTM-en; baner bygges stille og males med én bevisst operator; farge- og linjeinnstillinger varer til du klammer dem inn med q/Q; bilder og gjenbrukbar grafikk er XObjekter plassert ved å transformere et enhetskvadrat. Nesten hvert forvirrende gjengivelsesresultat lar seg redusere til en av de fem reglene. Vil du se hvordan disse grafikkoperatorene sitter inne i den større objektmodellen, sidedictionaryene og kryssreferansetabellen som peker på dem, dekker den tekniske oversikten over PDF-filstruktur det laget, og å bygge en enkel PDF fra bunnen av går gjennom bytene fra ende til ende. Teksttegning bor i sin egen operatorfamilie og har sine egne fallgruver, dekket i følgeartikkelen om PDF-tekst- og fonthåndtering

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