Teknisk artikel

Hvordan PDF-grafik fungerer: Indholdsstrømme og operatorer

En PDF-side gemmer ikke pixels, og den gemmer ikke et træ af formobjekter, som SVG gør. Den gemmer et program. Hver linje, kurve, fyld og placeret billede på siden er resultatet af at udføre en sekvens af operatorer i en indholdsstrøm, fra top til bund, mod en kørende grafiktilstand. Forstå dette ene faktum, og det meste af formatets adfærd stopper med at være overraskende: hvorfor et fyld kræver en separat malingsoperator, efter stien er bygget, hvorfor farver og linjebredder lækker fra én form til den næste, medmindre du sætter dem i parentes, hvorfor den samme tegnekode kan lande på helt forskellige steder efter en enkelt koordinattransformation. Dette er en rundtur i denne udførelsesmodel som defineret i ISO 32000: operatorerne du møder, når du åbner en indholdsstrøm, og reglerne, der bestemmer, hvad der vises på siden

Indholdsstrømmen er postfix-bytecode

En indholdsstrøm er en flad bytesekvens af operander efterfulgt af operatorer. Operander kommer først, operatoren, der forbruger dem, kommer sidst, hvilket er det modsatte af et funktionskald og identisk med en stakmaskine: skub tallene, og udsted derefter verbet. Der er ingen indlejring, ingen udtrykssyntaks, ingen variabler. En trekantomrids er fem linjer af dette:

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

Operatorerne er kortfattede med vilje. En rigtig side består af tusinder af disse, normalt komprimeret med FlateDecode. Prisen for den kompakthed er, at strømmen ikke bærer nogen struktur, du kan forespørge på: en fremviser kan ikke spørge "hvor er overskriften på denne side", den kan kun køre programmet og se, hvor blækket lander. Det er den grundlæggende årsag til, at tekstudtrækning fra vilkårlige PDF'er er svær

Origo er nederst til venstre, og Y vokser opad

Før nogen koordinat giver mening, skal du vide, hvor (0, 0) er. PDF placerer origo i nederste venstre hjørne af siden, med X stigende mod højre og Y stigende opad, målt i punkter med 72 punkter pr. tomme (ISO 32000-2 §8.3.2). På en US Letter-side sidder den øverste kant ved y = 792, ikke ved y = 0. Enhver, der ankommer fra skærmgrafik, hvor origo er øverst til venstre og Y vokser nedad, får dette baglæns i første forsøg og tegner den første linje ud over bunden af siden. Enheden er også uafhængig af mediet: 72 enheder er en tomme, uanset om siden gengives på en telefonskærm eller en fotosætter

De fleste sidetegningsbiblioteker arver denne konvention direkte. I HotPDF måler for eksempel TextOut og sti-kaldene alle fra nederst til venstre i punkter, så en værdi nær sidehøjden placerer indholdet øverst:

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

Den kaldsekvens kompileres ned til præcis de m, l og S operatorer som ovenfor. Biblioteket er en maskinskriver for indholdsstrømmen, intet mere, og at vide, hvad det udsender, er det, der lader dig ræsonnere om outputtet, når en form lander et sted, du ikke forventede

Byg stien, og mal den derefter

PDF adskiller stikonstruktion fra stimaling, og adskillelsen er ikke pedanteri. Du beskriver først en form med konstruktionsoperatorer, der ikke tilføjer noget synligt, og udsteder derefter en enkelt malingsoperator, der beslutter, hvad der skal gøres med den akkumulerede sti. Den samme trekant kan være et omrids, et solidt fyld eller begge dele, udelukkende afhængigt af det verb, du slutter med

Der er få konstruktionsoperatorer. m starter en ny delsti ved et punkt. l tilføjer et lige segment. c tilføjer en kubisk Bezier-kurve fra seks operander, to kontrolpunkter og et slutpunkt. re er en genvej, der tilføjer et helt rektangel fra en firedobbelt x, y, bredde og højde. h lukker den aktuelle delsti tilbage til starten. Ingen af dem sætter blæk på siden; de akkumulerer kun geometri

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

Det oprindelige eksempel brugte den nu forældede y-variant af kurveoperatoren; c med dens tre eksplicitte punkter er den form, du vil se i praksis, og den, du skal række ud efter. Når stien eksisterer, afslutter én malingsoperator den. Ordforrådet er lille og værd at lære udenad, fordi hver form på hver side slutter med en af disse:

  • S tegner stiomridset ved at bruge den aktuelle linjebredde og stregfarve
  • f udfylder det indre ved at bruge den aktuelle fyldfarve og nonzero winding-reglen
  • f* udfylder ved hjælp af even-odd-reglen, hvilket har betydning for selvgennemskærende former og former med huller
  • B udfylder og streger derefter i én operation; b lukker stien først
  • n maler ingenting, hvilket er, hvordan en sti bliver et beskæringsområde uden at efterlade et synligt mærke

Winding-reglen er den del, folk tager fejl af. Nonzero (f, B) tæller de fortegnede krydsninger af en stråle fra testpunktet og udfylder overalt, hvor tællingen ikke er nul, så et hul forbliver kun tomt, hvis dets delsti snor sig modsat den ydre. Even-odd (f*, B*) skifter ved hver krydsning uanset retning. Hvis en "donut"-form kommer solid ud, er den indre cirkel snoet samme vej som den ydre, og du skal enten vende den eller skifte til even-odd

Farve er en tilstand, ikke en parameter

Farve i en indholdsstrøm er klæbrig. Du indstiller en farve, og den forbliver indstillet, indtil du indstiller en anden eller gendanner en tidligere tilstand, hvilket er grunden til, at en farveændring uden parentes i stilhed farver alt tegnet efter den. PDF beholder også fyldfarve og stregfarve som to uafhængige indstillinger med små bogstaver for fyld og store bogstaver for streg. Enhedsfarverummene har hver deres stenografi:

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 passer til skærmoutput, DeviceCMYK er, hvad printproduktion forventer, og DeviceGray er det mindste valg til monokromt indhold. Enhedsrummene er praktiske, men ukalibrerede: den samme RGB-trio kan gengives forskelligt på to skærme, hvilket er det problem ICC-baserede farverum og PDF/A output-hensigter eksisterer for at løse. Til farvekritisk arbejde vælger du et kalibreret rum med cs og CS og sætter komponenter med sc og scn, men for almindelige dokumenter bærer enhedsstenografierne byrden. Et bibliotek indpakker disse i typede kald. HotPDF, for eksempel, tager en enkelt TColor og udsender de matchende operatorer:

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;

Grafiktilstanden og q/Q-stakken

Alt, der ikke er selve stien, bor i grafiktilstanden: nuværende transformationsmatrix, fyld- og stregfarver, linjebredde, bindestregsmønster, beskæringsområde, alfa. Tilstanden er global og foranderlig, så den eneste sikre måde at foretage en lokal ændring på er at gemme det hele, ændre det, tegne og rulle det tilbage. Det er, hvad q og Q gør. q skubber en kopi af den aktuelle tilstand ind på en stak; Q popper den, og kasserer enhver ændring foretaget siden den matchende 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

Ubalancerede q og Q er en almindelig måde, hvorpå en håndbygget eller sammensyet indholdsstrøm går galt. En omstrejfende q uden matchende Q efterlader stakken dyb, når siden slutter; en ekstra Q får den til at underløbe. Uanset hvad kan en fremviser bevare et gammelt klip eller transformere i kraft, og indhold forsvinder eller lander på det forkerte sted. Når grafik forsvinder uden nogen grund, stien kan forklare, skal du revidere tilstandsstakken først

CTM'en transformerer hver koordinat

Den aktuelle transformationsmatrix (CTM) sidder mellem tallene i dine operatorer og den faktiske side. Hver koordinat ganges med CTM'en, før noget tegnes, så at ændre matricen ændrer, hvor og hvordan alle efterfølgende tegninger vises uden at røre ved en enkelt stikoordinat. cm operatoren sammenkæder en ny matrix på den nuværende, og tager seks operander, der knytter sig til den affine matrix [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)

To ting spænder ben for folk. For det første komponerer cm snarere end erstatter, så transformationer akkumuleres og rækkefølge betyder noget: at skalere og derefter oversætte er ikke det samme som at oversætte og derefter skalere. For det andet drejer rotation og skalering sig omkring det nuværende origo, ikke midten af din form, så for at rotere noget på plads, oversætter du det til origo, roterer og derefter oversætter du det tilbage, alt sammen pakket ind i q/Q. Denne samme matrix er det, der placerer billeder, den sidste del, der er værd at se

Billeder og genanvendeligt indhold er XObjects

Rasterbilleder lever ikke inline i indholdsstrømmen. De gemmes som billed-XObjects, eksterne objekter med deres egen ordbog, der beskriver bredde, højde, bitdybde, farverum og kompressionsfilter, og indholdsstrømmen refererer kun til dem. Et JPEG-understøttet foto erklærer sig selv således:

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

Et billed-XObject tegner ind i enhedskvadratet: det optager altid området fra (0, 0) til (1, 1) i brugerrummet. Du sender det ikke en position eller størrelse. I stedet indstiller du CTM'en, så enhedskvadratet knytter sig til det rektangel, du vil have, og påkalder det derefter med Do. Det er grunden til, at placering af et billede altid er en transformation efterfulgt af en påkaldelse, pakket ind i en gem/gendan, så skalaen ikke bløder ind i den næste handling:

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

Den samme Do-mekanisme driver formular-XObjects, som indeholder en genanvendelig klump grafik, et logo eller et gentaget stempel, som deres egen indholdsstrøm med en afgrænsningsboks. Definer det én gang, påkald det mange gange med en forskellig CTM, og bytene optræder kun én gang i filen. De fleste biblioteker skjuler dette bag et enkelt placeringskald: HotPDF registrerer et bitmap med AddImage og placerer det med ShowImage, idet det tager en eksplicit x, y, bredde og højde i stedet for at bede dig bygge matricen i hånden:

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;

Under den ene linje skriver biblioteket billed-XObject-ordbogen, indstiller CTM'en til størrelse og positionering af enhedskvadratet, og udsender Do. Modellen nedenunder er den, der er værd at kende, for den forklarer hvert mærkeligt resultat: et strakt billede er en CTM med uoverensstemmende skalafaktorer, et logo, der er identisk på fyrre sider, er ét formular-XObject påkaldt fyrre gange, og et billede, der gengives på hovedet, er et fortegnsskift i matricen, ikke en korrupt fil

Hvor dette fører hen

Grafikmodellen er lille, når du først ser dens form. En indholdsstrøm er postfix-bytecode, der kører mod en foranderlig tilstand; koordinater starter nederst til venstre og passerer gennem CTM'en; stier bygges i stilhed og males med én bevidst operator; farve- og linjeindstillinger fortsætter, indtil du sætter dem i parentes med q/Q; billeder og genanvendelig grafik er XObjects placeret ved at transformere et enhedskvadrat. Næsten hvert forvirrende gengivelsesresultat reduceres til en af de fem regler. Hvis du vil se, hvordan disse grafikoperatorer sidder inde i den større objektmodel, sideordbøgerne og krydsreferencetabellen, der peger på dem, dækker den tekniske oversigt over PDF-filstruktur det lag, og opbygning af en simpel PDF fra bunden gennemgår bytene fra ende til anden. Teksttegning lever i sin egen operatorfamilie og har sine egne faldgruber, dækket i det ledsagende stykke om PDF-tekst og skrifttypehåndtering

De Delphi-tegnekald, der vises her, MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage og ShowImage, er en del af HotPDF Component til Delphi og C++Builder, som udsender disse indholdsstrømsoperatorer for dig