Teknisk artikel

Exportera PDF-sidor till SVG i Delphi med HotPDF

HotPDF exporterar en sida ur valfritt inläst PDF-dokument till fristående SVG-markup med ett enda anrop, BuildLoadedPageSVG, som returnerar hela SVG-dokumentet som en sträng. Den exporterade markupen innehåller sidans geometri, text som riktiga SVG text-element, inbäddade rasterbilder samt det konturtillstånd som PDF-operatorerna hade etablerat vid varje ritoperation

Just den sista delen är där de flesta egenbyggda konverterare tyst faller samman. Att omvandla en PDF-sida till SVG ser ut som ett koordinatproblem men visar sig vara ett tillståndsproblem. PDF är en stackmaskin vars grafiktillstånd ändras allteftersom innehållsströmmen tolkas; SVG är ett deklarativt träd vars element var och en bär sina egna presentationsattribut. Allt som tolken misslyckas med att ta en ögonblicksbild av när ett element skrivs ut försvinner helt enkelt ur utdata, och felet är tyst: du får giltig SVG som återger en subtilt felaktig sida

Varför konverteras inte en PDF-sida bara rakt av till SVG?

Tre inkonsekvenser gör konverteringen icke-trivial, och alla tre ger resultat som ser rimliga ut tills du jämför dem sida vid sida med originalet. Den första är y-axeln. PDF:s användarrymd växer uppåt från sidans nedre vänstra hörn; SVG växer nedåt från övre vänstra hörnet. En enda vändning på sidnivå korrigerar ritkoordinaterna men förstör då varje glyf, eftersom en vändning av hela ytan även speglar bokstavsformerna

Den andra inkonsekvensen är nedärvning. I PDF lägger q och Q ett grafiktillstånd på respektive tar bort det från en stack, vilket omfattar linjebredd, linjeände, linjefog, spetsgräns, streckmönster, streckfas och alfa. I SVG ärver ett element som inte anger ett attribut det från en förfadergrupp, vilket är en helt annan scoping-regel. En exportör som bara spårar den aktuella transformationsmatrisen och glömmer konturtillståndet låter det återställda tillståndet efter en Q läcka in i de element som följer

Den tredje är att PDF uttrycker flera saker genom konvention snarare än genom värde. Linjeändar och linjefogar är heltal, en linjebredd på noll betyder en hårlinje i enhetsrymden snarare än en osynlig linje, och stjärnvarianterna av ritoperatorerna ändrar fyllningsregeln istället för färgen. Var och en av dessa kräver en översättning, inte en kopiering

Ett anrop för normalfallet

För det vanliga arbetet att exportera sidor för en webbvisare, ett diff-verktyg eller en designöverlämning består API-ytan av en enda funktion. BuildLoadedPageSVG tar ett nollbaserat sidindex mot det aktuellt inlästa dokumentet och returnerar SVG-dokumentet som en AnsiString:

var
  Pdf: THotPDF;
  I: Integer;
  Svg: AnsiString;
  Output: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('statements.pdf', '') <= 0 then
      Exit;                     // LoadFromFile returnerar antalet sidor
    for I := 0 to Pdf.LoadedPageCount - 1 do
    begin
      Svg := Pdf.BuildLoadedPageSVG(I);
      if Length(Svg) = 0 then
        Continue;
      Output := TFileStream.Create(Format('page-%d.svg', [I + 1]), fmCreate);
      try
        Output.WriteBuffer(Svg[1], Length(Svg));
      finally
        Output.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Samma export exponeras av HotPDFs kommandoradsverktyg som kommandot export-svg, vilket är användbart i byggpipelines och regressionsskript där du vill ha en textbaserad, diffbar representation av en sida utan att skriva någon Pascal-kod. Eftersom SVG är text blir det ett naturligt komplement till rastervägen som beskrivs i att rendera en PDF-sida till en bitmapp: bitmappen visar hur sidan ser ut, SVG:n visar vad den är uppbyggd av

Hur mappas PDF-text till SVG text-element?

HotPDF bygger textmatriskedjan som prefix gånger CTM gånger textmatris gånger glyfvändning, där glyfvändningen är en högermultiplikation med matrix(1,0,0,-1,0,0). Den högra faktorn finns enbart för att upphäva den vertikala vändningen på sidnivå för glyfformerna, eftersom SVG-text ritad i en vänd lokal ram annars skulle visas upp och ner. Att lägga korrigeringen i matrisen istället för i specialfallshanterad kod innebär att roterad, speglad och skevad text alla blir korrekta utan extra grenar

Horisontell positionering använder SVG text-elementets flervärdessyntax för x, en koordinat per tecken, ackumulerad från varje glyfsteg plus den teckenavstånd Tc och ordavstånd Tw som gällde vid tillfället. Horisontell skalning Tz vävs in i a- och c-kolumnerna i textmatrisen istället för att skrivas ut separat, så en visare som ignorerar exotiska textattribut placerar ändå varje glyf där PDF:en satte den. Text som producerats genom komplex formgivning, beskriven i komplex skript-textformgivning, följer samma väg, eftersom formgivaren redan har löst upp kluster till positionerade glyfer vid den tidpunkt då innehållsströmmen tolkas

Rotation och bilder: två vändningar som lätt blir bakvända

En sida med en /Rotate-post som inte är noll behöver en förtransform som består av en vändning mot den roterade ytans höjd och en rotation uttryckt i y-upp-visningsrymden. De tre rotationsmatriserna är (0,-1,1,0,0,W) för 90 grader, (-1,0,0,-1,W,H) för 180 och (0,1,-1,0,H,0) för 270, där W och H är sidans dimensioner före rotation. Att härleda dessa för hand inbjuder till teckenfel på exakt tre ställen, så exportören sätter samman dem genom samma matrismultiplikationsrutin som hanterar alla andra transformer

Inbäddade bilder behöver en egen vändning, eftersom PDF:s bildrymd placerar den första samplingsraden vid övre kanten av enhetskvadraten, medan SVG:s image-element har en y-ner lokal ram. Den utskrivna transformen är därför CTM högermultiplicerad med matrix(1,0,0,-1,0,1). Att göra fel här ger vertikalt spegelvända fotografier på en annars perfekt sida, vilket är den typ av fel en granskare upptäcker direkt men ett automatiserat test ofta inte gör

Vad bevarar egentligen grafiktillståndsenheten?

HotPDF skickar konturtillstånds-operatorerna w, J, j, M och d genom ett separat, valfritt enhetsgränssnitt, så att konturtrohet lades till utan att ändra vtabellen för det befintliga innehållsdevice och utan att bryta binärkompatibiliteten för kod byggd mot tidigare versioner. Konkret innebär det att den exporterade SVG:n får översatta nyckelord istället för råa PDF-heltal:

// PDF:s heltalsuppräkningar blir SVG-nyckelordsattribut
//   linjeände  0, 1, 2  ->  butt, round, square
//   linjefog   0, 1, 2  ->  miter, round, bevel
//
// En linjebredd på noll betyder en hårlinje i enhetsrymden i PDF, så
// exportören skriver ut vector-effect="non-scaling-stroke" för att hålla
// konturen synlig och nära en enhetspixel bred efter CTM
//
// f* B* b* väljer even-odd-regeln och skriver ut fill-rule="evenodd",
// medan f B b behåller SVG:s standard nonzero winding

Att återställa tillståndet vid en Q täcker opacitet, linjebredd, ände, fog, spetsgräns, streckmönster och streckfas tillsammans. Nästlade Form XObjects tar en ögonblicksbild av och återställer samma fullständiga uppsättning vid sina gränser, så att en streckad kantlinje definierad inuti en stämpel inte kan läcka sitt mönster in i det sidinnehåll som följer. Om du redan spårar klippning och CTM-beteende av andra skäl är detta samma tillståndsmodell som förekommer i EMF- och WMF-vektorimport, fast i motsatt riktning

Gränser värda att känna till innan du levererar

Exportören är ärlig om sin omfattning, och att lära sig gränserna i förväg är billigare än att upptäcka dem i produktion. Färg når SVG-enheten via operatorerna rg, RG, g och G. Fyllningar som etablerats via ett färgrum plus scn, vilket är hur Separation-, DeviceN- och ICCBased-färger målas, når inte enheten som en färdiglöst RGB-trippel, så sidor som använder dekorfärger på detta sätt exporterar sin geometri men inte dessa färger. För tryckorienterade källor, rastrera istället eller platta ut dekorfärgerna först; själva målningsmodellen beskrivs i rendering av Separation- och DeviceN-dekorfärger

Två mindre noteringar sparar felsökningstid. Hexadecimala färglitteraler skrivs ut med versaler, så ett test som kontrollerar #ff0000 misslyckas mot ett fullt korrekt #FF0000. Dessutom är SVG-enheten referensräknad via sitt gränssnitt, vilket innebär att frisläppandet sker genom att låta gränssnittet gå ur scope snarare än genom att anropa Free på objektet, en distinktion som spelar roll om du utökar enheten för att skriva ut din egen markup tillsammans med sidinnehållet

SVG-export passar naturligt ihop med strukturell jämförelse när du behöver veta om ett genererat dokument verkligen ändrades mellan två byggen. Det bredare verktygspaketet kring inlästa dokument, från rendering till redigering till export, dokumenteras på sidan för HotPDF Delphi PDF-komponent