Technischer Artikel

PDF-Seiten in Delphi mit HotPDF als SVG exportieren

HotPDF exportiert eine Seite eines beliebigen geladenen PDF-Dokuments mit einem einzigen Aufruf, BuildLoadedPageSVG, als eigenständiges SVG-Markup, das als Zeichenkette zurückgegeben wird. Das exportierte Markup enthält die Seitengeometrie, Text als echte SVG-text-Elemente, eingebettete Rasterbilder und den Strichzustand, den die PDF-Operatoren bei jeder Zeichenoperation festgelegt hatten

Genau an diesem letzten Punkt scheitern die meisten selbstgebauten Konverter still und leise. Eine PDF-Seite in SVG umzuwandeln sieht wie ein Koordinatenproblem aus und entpuppt sich als Zustandsproblem. PDF ist eine Stack-Maschine, deren Grafikzustand sich während der Interpretation des Content-Streams ändert; SVG ist ein deklarativer Baum, dessen Elemente jeweils ihre eigenen Darstellungsattribute tragen. Alles, was der Interpreter im Moment der Erzeugung eines Elements nicht erfasst, fehlt im Ergebnis einfach – und der Fehler bleibt unbemerkt: Man erhält gültiges SVG, das eine subtil falsche Seite darstellt

Warum lässt sich eine PDF-Seite nicht einfach in SVG umwandeln?

Drei Diskrepanzen machen die Umwandlung alles andere als trivial, und alle drei erzeugen Ergebnisse, die plausibel wirken – bis man sie Seite an Seite mit dem Original vergleicht. Die erste betrifft die y-Achse. Der PDF-Nutzerraum wächst von der unteren linken Ecke der Seite nach oben; SVG wächst von der oberen linken Ecke nach unten. Eine einzelne Spiegelung auf Seitenebene korrigiert die Zeichenkoordinaten, zerstört dann aber jeden Glyphen, weil das Spiegeln der gesamten Zeichenfläche auch die Buchstabenformen spiegelt

Die zweite Diskrepanz betrifft die Vererbung. In PDF legen q und Q einen Grafikzustand auf einen Stack bzw. holen ihn zurück, der Linienbreite, Linienabschluss, Linienverbindung, Gehrungsgrenze, Strichmuster, Strichphase und Alpha umfasst. In SVG erbt ein Element, das ein Attribut nicht selbst benennt, dieses von einer übergeordneten Gruppe – eine völlig andere Scoping-Regel. Ein Exporteur, der nur die aktuelle Transformationsmatrix verfolgt und den Strichzustand vergisst, lässt den nach einem Q wiederhergestellten Zustand in die nachfolgenden Elemente durchsickern

Der dritte Punkt ist, dass PDF mehrere Dinge per Konvention statt per Wert ausdrückt. Linienabschlüsse und -verbindungen sind Ganzzahlen, eine Linienbreite von null bedeutet eine geräteraumbezogene Haarlinie statt einer unsichtbaren Linie, und die Stern-Varianten der Zeichenoperatoren ändern die Füllregel statt der Farbe. Jeder dieser Punkte braucht eine Übersetzung, keine Kopie

Ein Aufruf für den Standardfall

Für die gewöhnliche Aufgabe, Seiten für einen Web-Viewer, ein Diff-Tool oder eine Design-Übergabe zu exportieren, besteht die API-Oberfläche aus einer einzigen Funktion. BuildLoadedPageSVG nimmt einen nullbasierten Seitenindex bezogen auf das aktuell geladene Dokument entgegen und gibt das SVG-Dokument als AnsiString zurück:

var
  Pdf: THotPDF;
  I: Integer;
  Svg: AnsiString;
  Output: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('statements.pdf', '') <= 0 then
      Exit;                     // LoadFromFile gibt die Seitenanzahl zurück
    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;

Derselbe Export steht über das HotPDF-Kommandozeilenwerkzeug als Befehl export-svg zur Verfügung, was in Build-Pipelines und Regressionsskripten nützlich ist, wenn man eine textdiffbare Darstellung einer Seite haben möchte, ohne Pascal-Code zu schreiben. Da SVG Text ist, ergänzt es den in Rendern einer PDF-Seite in eine Bitmap beschriebenen Rasterweg auf natürliche Weise: Die Bitmap zeigt, wie die Seite aussieht, das SVG zeigt, woraus sie besteht

Wie wird PDF-Text auf SVG-Text-Elemente abgebildet?

HotPDF setzt die Textmatrizenkette als Präfix mal CTM mal Textmatrix mal Glyphenspiegelung zusammen, wobei die Glyphenspiegelung eine Rechtsmultiplikation mit matrix(1,0,0,-1,0,0) ist. Dieser rechte Faktor existiert einzig, um die vertikale Spiegelung auf Seitenebene für Glyphenformen aufzuheben, da sonst SVG-Text, der in einem gespiegelten lokalen Koordinatensystem gezeichnet wird, auf dem Kopf stehen würde. Die Korrektur in die Matrix statt in Spezialfall-Code zu legen bedeutet, dass gedrehter, gespiegelter und geschärter Text ohne zusätzliche Verzweigungen korrekt ausfällt

Die horizontale Positionierung nutzt die Mehrwert-Syntax des x-Attributs des SVG-text-Elements, eine Koordinate pro Zeichen, akkumuliert aus jedem Glyphenvorschub zuzüglich der zum jeweiligen Zeitpunkt geltenden Zeichenabstands Tc und Wortabstands Tw. Die horizontale Skalierung Tz wird in die Spalten a und c der Textmatrix eingerechnet statt separat ausgegeben, sodass ein Viewer, der exotische Textattribute ignoriert, jeden Glyphen trotzdem dort platziert, wo ihn das PDF vorgesehen hat. Text, der durch komplexes Shaping erzeugt wurde – behandelt in Text-Shaping für komplexe Schriftsysteme – durchläuft denselben Pfad, weil der Shaper Cluster bereits zu positionierten Glyphen aufgelöst hat, wenn der Content-Stream interpretiert wird

Rotation und Bilder: zwei Spiegelungen, die man leicht vertauscht

Eine Seite mit einem von null verschiedenen /Rotate-Eintrag benötigt eine Vor-Transformation, die aus einer Spiegelung gegen die rotierte Canvashöhe und einer im y-nach-oben-Anzeigeraum ausgedrückten Rotation besteht. Die drei Rotationsmatrizen sind (0,-1,1,0,0,W) für 90 Grad, (-1,0,0,-1,W,H) für 180 Grad und (0,1,-1,0,H,0) für 270 Grad, wobei W und H die Seitenabmessungen vor der Rotation sind. Diese von Hand herzuleiten lädt an genau drei Stellen zu Vorzeichenfehlern ein, daher setzt der Exporteur sie über dieselbe Matrixmultiplikationsroutine zusammen, die jede andere Transformation behandelt

Eingebettete Bilder brauchen eine eigene Spiegelung, weil der PDF-Bildraum die erste Abtastzeile an der oberen Kante des Einheitsquadrats platziert, während das SVG-image-Element ein y-nach-unten-lokales Koordinatensystem trägt. Die ausgegebene Transformation ist daher die CTM, rechtsmultipliziert mit matrix(1,0,0,-1,0,1). Wird dies falsch gemacht, entstehen vertikal gespiegelte Fotografien auf einer ansonsten fehlerfreien Seite – ein Defekt, den ein Prüfer sofort erkennt, ein automatisierter Test aber oft nicht

Was bewahrt das Grafikzustandsgerät tatsächlich?

HotPDF leitet die Strichzustand-Operatoren w, J, j, M und d über eine separate optionale Geräteschnittstelle weiter, sodass die Strichtreue hinzugefügt wurde, ohne die vtable des bestehenden Content-Geräts zu ändern und ohne die Binärkompatibilität für gegen ältere Versionen erstellten Code zu brechen. Konkret erhält das exportierte SVG übersetzte Schlüsselwörter statt roher PDF-Ganzzahlen:

// PDF-Ganzzahl-Enumerationen werden zu SVG-Schlüsselwort-Attributen
//   Linienabschluss  0, 1, 2  ->  butt, round, square
//   Linienverbindung 0, 1, 2  ->  miter, round, bevel
//
// Eine Linienbreite von null bedeutet in PDF eine geräteraumbezogene
// Haarlinie, daher gibt der Exporteur vector-effect="non-scaling-stroke"
// aus, um den Strich sichtbar und nach der CTM nahe einem Gerätepixel
// breit zu halten
//
// f* B* b* wählen die Even-Odd-Regel und geben fill-rule="evenodd" aus,
// während f B b die SVG-Standardregel des Nonzero-Windings beibehalten

Die Zustandswiederherstellung bei einem Q deckt Deckkraft, Linienbreite, Abschluss, Verbindung, Gehrungsgrenze, Strichmuster und Strichphase gemeinsam ab. Verschachtelte Form-XObjects erfassen und stellen denselben vollständigen Satz an ihren Grenzen wieder her, sodass ein innerhalb eines Stempels definierter gestrichelter Rahmen sein Muster nicht in den nachfolgenden Seiteninhalt durchsickern lassen kann. Wer Clipping- und CTM-Verhalten aus anderen Gründen bereits verfolgt, findet dasselbe Zustandsmodell in EMF- und WMF-Vektorimport wieder – dort in die entgegengesetzte Richtung laufend

Grenzen, die man vor dem Einsatz kennen sollte

Der Exporteur ist ehrlich hinsichtlich seines Umfangs, und die Grenzen im Voraus zu kennen ist günstiger, als sie in der Produktion zu entdecken. Farbe erreicht das SVG-Gerät über die Operatoren rg, RG, g und G. Füllungen, die über einen Farbraum plus scn festgelegt wurden – so werden Separation-, DeviceN- und ICCBased-Farben gemalt – kommen nicht als aufgelöstes RGB-Tripel beim Gerät an, sodass Seiten, die Schmuckfarben auf diese Weise nutzen, ihre Geometrie exportieren, aber nicht diese Farben. Für druckorientierte Quellen stattdessen rastern oder die Schmuckfarben vorher flachdrücken; das Zeichenmodell selbst wird in Rendern von Separation- und DeviceN-Schmuckfarben behandelt

Zwei kleinere Hinweise sparen Debugging-Zeit. Hexadezimale Farbliterale werden in Großbuchstaben ausgegeben, sodass ein Test, der #ff0000 erwartet, gegen ein völlig korrektes #FF0000 fehlschlägt. Und das SVG-Gerät wird über seine Schnittstelle referenzgezählt, was bedeutet, dass es freizugeben genügt, die Schnittstelle aus dem Gültigkeitsbereich laufen zu lassen, statt Free auf dem Objekt aufzurufen – eine Unterscheidung, die wichtig wird, wenn man das Gerät erweitert, um eigenes Markup neben dem Seiteninhalt auszugeben

SVG-Export lässt sich natürlich mit strukturellem Vergleich kombinieren, wenn man wissen muss, ob sich ein generiertes Dokument zwischen zwei Builds tatsächlich verändert hat. Das breitere Toolkit rund um geladene Dokumente, vom Rendern über das Bearbeiten bis zum Export, ist auf der HotPDF-Delphi-PDF-Komponentenseite dokumentiert