Teknisk artikel

Hur PDF-grafik fungerar: Innehållsströmmar och operatörer

En PDF-sida lagrar inte pixlar och den lagrar inte ett träd av formobjekt på samma sätt som SVG gör. Den lagrar ett program. Varje linje, kurva, fyllning och placerad bild på sidan är resultatet av att en sekvens av operatörer körs i en innehållsström, uppifrån och ner, mot ett pågående grafiktillstånd. Förstå det faktumet och det mesta av formatets beteende slutar att vara överraskande: varför en fyllning behöver en separat målningsoperatör efter att banan har byggts, varför färger och linjebredder läcker från en form till nästa om du inte ramar in dem, varför samma ritkod kan hamna på helt olika platser efter en enda koordinattransformation. Detta är en rundtur i den exekveringsmodellen som definieras i ISO 32000: operatörerna du möter när du öppnar en innehållsström, och reglerna som avgör vad som visas på sidan

Innehållsströmmen är postfix-bytekod

En innehållsström är en platt bytesekvens av operander följt av operatörer. Operanderna kommer först, operatören som konsumerar dem kommer sist, vilket är omvänt mot ett funktionsanrop och identiskt med en stackmaskin: skjut in siffrorna, utfärda sedan verbet. Det finns ingen nästling, ingen uttryckssyntax, inga variabler. En triangelkontur är fem rader av detta:

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

Operatörerna är korta och koncisa avsiktligt. En riktig sida består av tusentals av dessa, vanligtvis komprimerade med FlateDecode. Kostnaden för den kompaktheten är att strömmen inte bär någon struktur du kan fråga efter: en visare kan inte fråga "var är rubriken på den här sidan", den kan bara köra programmet och se vilket bläck som hamnar var. Det är den grundläggande anledningen till att textutvinning från godtyckliga PDF-filer är svårt

Origo är nere till vänster, och Y växer uppåt

Innan någon koordinat blir begriplig måste du veta var (0, 0) är. PDF placerar origo i sidans nedre vänstra hörn, med X som ökar åt höger och Y som ökar uppåt, mätt i punkter med 72 punkter per tum (ISO 32000-2 §8.3.2). På en US Letter-sida sitter överkanten vid y = 792, inte vid y = 0. Alla som kommer från skärmgrafik, där origo är uppe till vänster och Y växer nedåt, får detta bakvänt på första försöket och ritar den första linjen utanför sidans underkant. Enheten är också oberoende av mediet: 72 enheter är en tum oavsett om sidan renderas till en telefonskärm eller en bildsättare

De flesta sidritningsbibliotek ärver denna konvention direkt. I HotPDF, till exempel, mäter TextOut och bananropen från nedre vänstra hörnet i punkter, så ett värde nära sidhöjden placerar innehåll i toppen:

// 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 anropssekvensen kompileras ner till exakt operatörerna m, l och S ovan. Biblioteket är en maskinskrivare för innehållsströmmen, inget mer, och att veta vad det avger är det som låter dig resonera kring resultatet när en form hamnar någonstans där du inte förväntade dig

Bygg banan, måla den sedan

PDF separerar banans konstruktion från dess målning, och separationen är inte pedanteri. Du beskriver först en form med konstruktionsoperatörer som inte lägger till något synligt, och utfärdar sedan en enda målningsoperatör som avgör vad som ska göras med den ackumulerade banan. Samma triangel kan vara en kontur, en solid fyllning, eller båda, beroende enbart på vilket verb du avslutar med

Konstruktionsoperatörerna är få. m startar en ny underbana vid en punkt. l lägger till ett rakt segment. c lägger till en kubisk Bezier-kurva från sex operander, två kontrollpunkter och en slutpunkt. re är en genväg som lägger till en hel rektangel från en kvadrupel av x, y, bredd och höjd. h stänger den aktuella underbanan tillbaka till dess start. Ingen av dem lägger bläck på sidan; de ackumulerar bara 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 ursprungliga exemplet använde den numera föråldrade y-varianten av kurvoperatören; c med sina tre explicita punkter är formen du kommer att se i praktiken och den du bör använda. När banan väl existerar avslutar en målningsoperatör den. Ordförrådet är litet och värt att memorera, eftersom varje form på varje sida slutar med en av dessa:

  • S ritar banans kontur med den aktuella linjebredden och linjefärgen
  • f fyller interiören med den aktuella fyllnadsfärgen och regeln om nollskild lindning (nonzero winding rule)
  • f* fyller med jämn-udda-regeln (even-odd rule), vilket spelar roll för självöverlappande former och former med hål
  • B fyller och ritar sedan konturen i en operation; b stänger banan först
  • n målar ingenting, vilket är hur en bana blir en beskärningsregion utan att lämna ett synligt märke

Lindningsregeln är den del folk gör fel på. Nollskild (f, B) räknar de signerade korsningarna av en stråle från testpunkten och fyller där räkningen inte är noll, så ett hål förblir bara tomt om dess underbana lindas motsatt den yttre. Jämn-udda (f*, B*) växlar vid varje korsning oavsett riktning. Om en "munk"-form (donut) blir solid, lindas den inre cirkeln på samma sätt som den yttre, och du måste antingen vända den eller byta till jämn-udda

Färg är ett läge, inte en parameter

Färg i en innehållsström är klibbigt. Du ställer in en färg och den förblir inställd tills du ställer in en annan eller återställer ett tidigare tillstånd, vilket är anledningen till att en oinramad färgändring i tysthet tonar allt som ritas efter den. PDF håller också fyllnadsfärg och linjefärg som två oberoende inställningar, med gemena operatörer för fyllning och versala för linje. Enheternas färgrymder har var och en sin egen 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 passar för skärmutdata, DeviceCMYK är vad tryckproduktion förväntar sig, och DeviceGray är det minsta valet för monokromt innehåll. Enhetsrymderna är bekväma men okalibrerade: samma RGB-trippel kan renderas olika på två bildskärmar, vilket är problemet som ICC-baserade färgrymder och PDF/A-utgångsavsikter finns till för att lösa. För färgkritiskt arbete väljer du en kalibrerad rymd med cs och CS och ställer in komponenter med sc och scn, men för vanliga dokument bär enhetsgenvägarna lasten. Ett bibliotek omsluter dessa i typade anrop. HotPDF, till exempel, tar en enda TColor och avger de matchande operatörerna:

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;

Grafiktillståndet och q/Q-stacken

Allt som inte är själva banan lever i grafiktillståndet: aktuell transformationsmatris, fyllnads- och linjefärger, linjebredd, streckmönster, beskärningsregion, alfa. Tillståndet är globalt och föränderligt, så det enda säkra sättet att göra en lokal förändring är att spara hela alltet, modifiera det, rita, och rulla tillbaka det. Det är vad q och Q gör. q skjuter in en kopia av det aktuella tillståndet på en stack; Q poppar den och kastar varje ändring som gjorts sedan matchande 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

Obalanserade q och Q är ett vanligt sätt som en handbyggd eller ihopsatt innehållsström går fel på. Ett vilsekommet q utan matchande Q lämnar stacken djup när sidan slutar; ett extra Q orsakar underströmning. Oavsett vilket kan en visare behålla en gammal beskärning eller transformation i kraft, och innehåll försvinner eller hamnar på fel plats. När grafik försvinner utan någon anledning som banan kan förklara, granska tillståndsstacken först

CTM transformerar varje koordinat

Den aktuella transformationsmatrisen (CTM) sitter mellan siffrorna i dina operatörer och den faktiska sidan. Varje koordinat multipliceras med CTM innan något ritas, så att ändra matrisen ändrar var och hur all efterföljande ritning visas utan att röra en enda bankoordinat. cm-operatören sammanfogar en ny matris på den aktuella, och tar sex operander som mappar till den affina matrisen [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)

Två saker brukar snubbla till det för folk. För det första komponerar cm istället för att ersätta, så transformationer ackumuleras och ordningen spelar roll: att skala och sedan translatera är inte detsamma som att translatera och sedan skala. För det andra pivoterar rotation och skalning runt det aktuella origot, inte i mitten av din form, så för att rotera något på plats translaterar du det till origo, roterar, och translaterar sedan tillbaka, allt inslaget i q/Q. Samma matris är det som placerar bilder, den sista pusselbiten värd att se

Bilder och återanvändbart innehåll är XObjects

Rasterbilder lever inte inline i innehållsströmmen. De lagras som bild-XObjects, externa objekt med sin egen ordbok som beskriver bredd, höjd, bitdjup, färgrymd och komprimeringsfilter, och innehållsströmmen refererar bara till dem. Ett JPEG-baserat foto deklarerar sig självt så här:

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

Ett bild-XObject ritar i enhetskvadraten: det upptar alltid regionen från (0, 0) till (1, 1) i användarrymden. Du skickar inte med någon position eller storlek. Istället ställer du in CTM så att enhetskvadraten mappar till den rektangel du vill ha, och anropar den sedan med Do. Det är därför att placera en bild alltid är en transformation följd av ett anrop, insvept i en spara/återställ så att skalningen inte blöder in i nästa operation:

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

Samma Do-mekanism driver form-XObjects, som håller en återanvändbar bit grafik, en logotyp eller en upprepad stämpel, som sin egen innehållsström med en avgränsningsram. Definiera den en gång, anropa den många gånger med en annan CTM, och byten visas i filen endast en gång. De flesta bibliotek döljer detta bakom ett enda placeringsanrop: HotPDF registrerar en bitmapp med AddImage och placerar den med ShowImage, vilket tar en explicit x, y, bredd och höjd istället för att be dig bygga matrisen för hand:

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 ena raden skriver biblioteket bild-XObject-ordboken, ställer in CTM till storlek och position för enhetskvadraten, och avger Do. Modellen därunder är den som är värd att veta, eftersom den förklarar varje märkligt resultat: en utsträckt bild är en CTM med felaktiga skalfaktorer, en logotyp som är identisk på fyrtio sidor är ett form-XObject som anropas fyrtio gånger, och en bild som renderas upp och ner är en teckenvändning i matrisen, inte en skadad fil

Vart detta leder

Grafikmodellen är liten när du väl ser dess form. En innehållsström är postfix-bytekod som körs mot ett föränderligt tillstånd; koordinater börjar nere till vänster och passerar genom CTM; banor byggs i tysthet och målas med en medveten operatör; färg- och linjeinställningar kvarstår tills du ramar in dem med q/Q; bilder och återanvändbar grafik är XObjects som placeras genom att transformera en enhetskvadrat. Nästan varje förvirrande renderingsresultat reduceras till en av dessa fem regler. Om du vill se hur dessa grafikoperatörer sitter inuti den större objektmodellen, sidordböckerna och korsreferenstabellen som pekar på dem, täcker den tekniska översikten av PDF-filstruktur det lagret, och att bygga en enkel PDF från grunden går igenom byten från början till slut. Textritning lever i sin egen operatörsfamilj och har sina egna fallgropar, vilket täcks i den tillhörande artikeln om PDF-text och typsnittshantering

Delphi-ritningsanropen som visas här, MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage och ShowImage, är en del av HotPDF-komponenten för Delphi och C++Builder, som avger dessa innehållsströmsoperatörer åt dig