Technischer Artikel

Axiale und radiale PDF-Gradients in Delphi mit PDFium

PDFium Component erzeugt axiale und radiale PDF-Gradients in Delphi über TPdf.SaveAsShadingCreated. Sie beschreiben jeden Gradient in einem TPdfShadingCreate-Record (Typ, Color Space, Koordinaten, Farben oder Stops, Zielseite), und die Methode schreibt eine verifizierte Kopie, in der jede Füllung ein echtes ISO-32000-1-Shading-Dictionary ist, registriert im /Resources /Shading-Dictionary der Seite und gemalt vom sh-Operator, sodass PDFium es nativ rendert

Das Bedürfnis taucht in Report- und Zertifikats-Generatoren auf. Ein Designer liefert ein Mock-up mit einem sanften Blau-Weiß-Kopfbalken oder einem radialen Schein hinter einem Siegel, und die Page-Object-API, die Text, Pfade und Bilder abdeckt, hat dafür nichts. Sie können einen Gradient mit hundert dünnen Rechtecken in leicht unterschiedlichen Farben faken, aber das Ergebnis bandet sichtbar beim Zoomen, bläht den Content-Stream auf und druckt schlecht. PDF hat weiche Shadings seit 1.3; die Arbeit besteht darin, sie so zu schreiben, dass ein Renderer sie tatsächlich zeichnet

Warum schreibt PDFium Component Gradients in einem separaten Save?

TPdf.SaveAsShadingCreated ist eine Save-Operation statt eines Page-Object-Aufrufs, weil PDFiums öffentliches ABI keine Funktion hat, die ein Shading-Dictionary erzeugt oder einen sh-Operator emittiert. Die Page-Object-Schicht, die beim Erstellen von PDFs von Grund auf mit PDFium Component verwendet wird, baut Pfade, Text und Bilder — und dort hört sie auf. Shadings leben eine Ebene tiefer, im Objektgraphen und in der Content-Stream-Syntax

Die Methode arbeitet also in Stufen. PDFium speichert zunächst das offene Dokument nativ, was jede über die normale API gemachte Bearbeitung hält. PDFium Component schreibt dann diese gestaffelte Ausgabe um: Es alloziert ein indirektes Objekt pro Shading, ergänzt den Ressourceneintrag an jeder Zielseite, hängt einen neuen Content-Stream an die /Contents der Seite an und schreibt eine frische Cross-Reference-Tabelle. Bevor irgendetwas Ihren Stream oder Ihre Datei erreicht, wird das Ergebnis mit strikter Cross-Reference-Prüfung neu geladen, und die Methode gibt False mit einer Meldung im Report zurück, wenn dieses Neuladen scheitert. Der Datei-Overload schreibt zuerst in eine temporäre Datei, sodass ein fehlgeschlagener Save kein bestehendes Ziel zerschlägt

Einen Zweifarben-Gradient axial aufbauen

Ein axiales Shading (ISO 32000-1 §8.7.4.5.3, Typ 2) vermischt Farben entlang einer Linie von x0 y0 nach x1 y1, Coords nimmt also vier Werte. Ein radiales Shading (§8.7.4.5.4, Typ 3) vermischt zwischen zwei Kreisen und nimmt sechs: x0 y0 r0 x1 y1 r1. Mit schlichtem StartColor und EndColor emittiert PDFium Component eine Type-2-Exponentialfunktion (§7.10.3) mit /N 1 — eine geradlinige lineare Interpolation — und setzt /Extend [true true], sodass die Endfarben über beide Enden der Achse hinaus weiterlaufen

PDFium-Component-Axial- und Radial-Shading-Geometrie für Delphi: ein Typ-2-Gradient vermischt StartColor zu EndColor entlang der Linie von x0 y0 nach x1 y1, mit /Extend true true, das über beide Enden hinausläuft, während ein Typ-3-Gradient zwischen einem Startkreis mit Radius r0 und einem Endkreis mit Radius r1 vermischt
PDFium malt das Shading über alles, was der Clip erlaubt: Typ-2-Coords platzieren die Mischachse von x0 y0 nach x1 y1, Typ-3-Coords platzieren die beiden Kreise, und /Extend [true true] trägt beide Endfarben über die Geometrie hinaus
uses
  System.SysUtils, PDFium, FPdfCompress;

procedure AddHeaderBand(const InFile, OutFile: string);
var
  Pdf: TPdf;
  Fills: TPdfShadingCreateShadings;
  Report: TPdfShadingCreateReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := InFile;
    Pdf.Active := True;

    SetLength(Fills, 1);
    Fills[0] := Default(TPdfShadingCreate);
    Fills[0].ShadingType := pstAxial;
    Fills[0].ColorSpace := pscDeviceRGB;
    Fills[0].Coords := [0, 792, 0, 640];        // x0 y0 x1 y1, von oben nach unten
    Fills[0].StartColor := [0.12, 0.35, 0.75];  // dunkelblau an der Oberkante
    Fills[0].EndColor := [1, 1, 1];             // weiß weiter unten
    Fills[0].PageNumber := 1;                   // 1-basiert
    Fills[0].ResourceName := 'HeaderBand';      // optional, Default ist Sh1, Sh2 ...

    if not Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
      raise Exception.Create('Shading creation failed: ' + Report.ErrorMessage);
  finally
    Pdf.Free;
  end;
end;

Die Komponentenzahl jeder Farbe muss zum Color Space passen: eine für pscDeviceGray und pscCalGray, drei für pscDeviceRGB, pscCalRGB und pscLab, vier für pscDeviceCMYK. Bei pscICCBased kommt die Zahl aus den Profil-Bytes, die Sie in IccProfile übergeben und die 1, 3 oder 4 Komponenten offenlegen müssen; das Profil wird als eigener ICC-Stream mit passendem /N eingebettet. Kalibrierte Spaces nehmen optionale CalWhitePoint (drei Werte, positives Y), CalGamma (ein Wert für CalGray, drei für CalRGB) und, für Lab, ein sechswertiges LabRange entgegen. Jede Koordinate und Komponente wird auf NaN und Unendlichkeit geprüft, bevor ein Byte geschrieben wird

Beachten Sie, dass Koordinaten im Default-User-Space liegen, mit dem Ursprung unten links auf der Seite. Denken Sie daran, dass die Achse nur definiert, wo die Mischung passiert: Die gemalte Fläche ist das, was der Clip erlaubt — was wichtiger klingt, als es ist, wie die nächsten Abschnitte zeigen

Multi-Stop-Gradients kompilieren zu einer Type-3-Stitching-Function

PDFium Component verwandelt ein Stops-Array in die Funktion, die ein PDF-Reader erwartet: Zwei Stops werden eine einzelne Type-2-Function, und 3 bis 256 Stops werden eine Type-3-Stitching-Function (ISO 32000-1 §7.10.4) aus einem linearen Type-2-Segment pro benachbartem Paar. Jeder TPdfShadingCreateStop trägt eine Position und eine Color. Ist Stops nicht leer, ersetzt es StartColor und EndColor vollständig

Die Stitching-Function braucht drei Arrays, die von Hand leicht subtil falsch zu bekommen sind. /Functions hält die Segmente, /Bounds hält nur die inneren Stop-Positionen (k Segmente brauchen k-1 Bounds), und /Encode mappt jede Subdomain zurück auf [0 1], sodass jedes Segment seinen vollen Eingabebereich sieht. Die Komponente leitet alle drei aus Ihren Stops ab, weshalb sie streng mit der Eingabe ist: Positionen müssen endlich, streng aufsteigend, bei 0 beginnend und bei 1 endend sein (innerhalb von 1e-6). Eine Stop-Liste, die bei 0,1 beginnt, würde einen Teil der Domain ohne definierte Funktion lassen, und die Methode weist sie ab, statt zu raten

function Stop(Position: Double; const Rgb: array of Double): TPdfShadingCreateStop;
var
  I: Integer;
begin
  Result.Position := Position;
  SetLength(Result.Color, Length(Rgb));
  for I := 0 to High(Rgb) do
    Result.Color[I] := Rgb[I];
end;

procedure BuildSealGlow(var Fill: TPdfShadingCreate);
begin
  Fill := Default(TPdfShadingCreate);
  Fill.ShadingType := pstRadial;
  Fill.ColorSpace := pscDeviceRGB;
  // x0 y0 r0 x1 y1 r1: ein Punkt im Siegelzentrum, wachsend zu einem 90-pt-Kreis
  Fill.Coords := [450, 160, 0, 450, 160, 90];
  Fill.Stops := [Stop(0.0, [1.00, 0.95, 0.70]),
                 Stop(0.4, [0.95, 0.75, 0.30]),
                 Stop(1.0, [1.00, 1.00, 1.00])];
  Fill.PageNumber := 1;
end;

Für einen Dreistoppen-Gradient produziert das /Functions mit zwei Type-2-Segmenten, /Bounds [0.4] und /Encode [0 1 0 1]. Die Segmentgrenzen sind harte Farbverbindungen, also glättet eine Stop-Liste nicht über sie hinweg; wirkt eine Verbindung zu scharf, ergänzen Sie Stops, statt zu erwarten, dass der Reader Krümmung interpoliert

PDFium-Component-Multi-Stop-Gradient-Kompilation in Delphi: drei Stops bei den Positionen 0.0, 0.4 und 1.0 werden eine Type-3-Stitching-Function mit einem Type-2-Segment pro benachbartem Stop-Paar, /Bounds [0.4] für die innere Position und /Encode [0 1 0 1], das jede Subdomain auf den vollen Bereich zurückmappt
Jedes benachbarte Stop-Paar wird ein Type-2-Segment innerhalb einer Type-3-Function; die abgeleiteten /Bounds- und /Encode-Arrays sind das, was handgeschriebene Stitching-Functions meist falsch bekommen

Warum zeichnet PDFium still nichts?

PDFium rendert ein neu erzeugtes Shading nur, wenn zwei Details stimmen: /ShadingType muss ein Integer sein, und der sh-Aufruf muss innerhalb eines Clipping-Pfads sitzen. Eines davon falsch, und die Datei öffnet sich, validiert strukturell und zeigt eine unberührte Seite. Kein Fehler, keine Warnung, keine Log-Zeile

Falle eins: /ShadingType ist eine Zahl, kein Name

ISO 32000-1 Tabelle 78 definiert /ShadingType als Integer: 2 für axial, 3 für radial. Die erste Version dieses Writers emittierte /ShadingType /Axial, weil es sich in einem Hex-Dump besser las. PDFium liest den Schlüssel mit einem Integer-Getter, bekommt 0 für einen Namen, klassifiziert das Shading als ungültig, scheitert am Laden und überspringt den sh-Operator ohne Murren. Die Seite renderte leer, und nichts in PDFium sagte warum. Die Ursache wurde erst sichtbar, nachdem ein minimales handgeschriebenes PDF gebaut und in einem zweiten, unabhängigen Renderer geöffnet wurde, der einen unbekannten Shading-Typ von 0 meldete. Die Lehre verallgemeinert auf jeden PDF-Writer: Wenn die Spezifikation eine Enumeration als Integer typisiert, schreiben Sie den Integer, so verlockend ein selbstbeschreibender Name auch aussieht

Falle zwei: Ein Content-Stream, der nur sh hält, hat keine Bounding Box

Der sh-Operator (ISO 32000-1 §8.7.4.2) malt das Shading über die aktuelle Clipping-Region, also begrenzt auf dem Papier die eigene Ausdehnung der Seite. PDFium baut jedoch ein Shading-Page-Object, dessen Rechteck entweder aus dem aktiven Clip-Pfad oder aus der Bounding Box stammt, die frühere Zeichenoperatoren im selben Stream akkumuliert haben, und der Renderer verwirft Objekte mit leerem Rechteck. Ein frisch angehängter Stream, der nichts als /Sh1 sh enthält, hat beides nicht, also wird der Gradient vor der Rasterisierung fallengelassen. TPdf.SaveAsShadingCreated liest daher die Crop-Box jeder Seite (mit Fallback auf die Media-Box, dann auf die Seitengröße) und hüllt jeden Aufruf in einen expliziten re W n-Clip. Auf einer US-Letter-Seite mit Ursprung bei null sieht der angehängte Stream so aus:

q
0 0 612 792 re W n
/HeaderBand sh
Q

Das Paar q/Q isoliert den Clip und jede Transformation vom bestehenden Seiteninhalt, sodass nichts, was später auf der Seite gezeichnet wird, sie erbt. Schreiben Sie Shading-Aufrufe selbst in einem anderen Tool, kopieren Sie diese Form: Ein expliziter Clip im selben Stream ist billig und entfernt die Abhängigkeit von dem, was vorher kam

Warum ein gültig aussehendes PDF-Shading in PDFium nichts zeichnet: ein als Name Axial geschriebenes /ShadingType wird von einem Integer-Getter als 0 gelesen und der sh-Operator still übersprungen, und ein Content-Stream, der nur sh hält, hat keine Bounding Box, bis ein expliziter re-W-n-Clip ihm ein Rechteck gibt
Beide unsichtbaren Shading-Fehlschläge sind in PDFium still, also schreibt SaveAsShadingCreated den Integer-ShadingType selbst und hüllt jeden sh-Aufruf in einen Crop-Box-re-W-n-Clip

Wie interagiert eine eigene Gradient-Matrix mit dem Clip?

Eine Gradient-Matrix wird in PDFium Component als cm-Operator angewendet, platziert nach dem Clip und vor sh, sodass der Clip vollseitig bleibt, während nur die Gradient-Geometrie transformiert wird. Setzen Sie HasMatrix auf True und füllen MatrixA bis MatrixF — dieselben sechs Zahlen wie jede PDF-Transformationsmatrix. Die Werte müssen endlich und die Determinante ungleich null sein, denn eine singuläre Matrix würde das Shading auf eine Linie kollabieren. Die Komponente verzeichnet die sechs Werte auch als /Matrix im Shading-Dictionary, aber bei einem direkten sh-Aufruf ist es das cm im Content-Stream, das das Koordinatensystem setzt, in dem PDFium malt

Die Reihenfolge zählt hier, und sie ist die dritte Falle. Die re-Operanden werden in der aktuellen Transformationsmatrix interpretiert. Setzen Sie das cm zuerst, wird das seitengroße Clip-Rechteck zusammen mit dem Gradient skaliert, rotiert oder geschert, sodass eine Matrix, die eine Ellipse staucht, auch die sichtbare Fläche staucht und Teile der Seite ihre Füllung verlieren. Erst in untransformiertem User-Space zu clippen und dann die Matrix zu konkatenieren liefert das erwartete Ergebnis: einen transformierten Gradient über der ganzen Seite. Der Leitfaden zu Form-XObject-Stamps und ihrer Sechs-Zahlen-Matrix geht durch, was jede Matrixkomponente tut, falls Sie eine bauen müssen

procedure MakeEllipticalGlow(var Fill: TPdfShadingCreate);
begin
  BuildSealGlow(Fill);       // radiale Füllung zentriert bei 450,160
  Fill.Coords := [0, 0, 0, 0, 0, 90];  // Kreise im Ursprung
  Fill.HasMatrix := True;
  Fill.MatrixA := 1.6;       // horizontal strecken
  Fill.MatrixB := 0;
  Fill.MatrixC := 0;
  Fill.MatrixD := 0.8;       // vertikal stauchen
  Fill.MatrixE := 450;       // dann zum Siegelzentrum verschieben
  Fill.MatrixF := 160;
end;

Den Report lesen und die Grenzen kennen

TPdfShadingCreateReport sagt Ihnen, was geschrieben wurde: SourceByteCount und OutputByteCount, CreatedShadingObjectNumbers (eine Objektnummer pro Eingabeeintrag, in Reihenfolge), AttachedShadingCount, Succeeded und ErrorMessage. Validierungsfehler nennen den Eintrag und das Feld, etwa eine Stop-Liste, die nicht streng aufsteigend ist, lohnen sich also wörtlich zu loggen

if Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
  Writeln(Format('%d shading(s) attached, %d -> %d bytes',
    [Report.AttachedShadingCount, Report.SourceByteCount,
     Report.OutputByteCount]))
else
  Writeln('Rejected: ' + Report.ErrorMessage);

Das Profil ist bewusst begrenzt. Es erzeugt nur axiale und radiale Shadings; funktionsbasierte und Mesh-Shadings (Typen 1 und 4 bis 7) sowie Pattern-Color-Spaces werden abgewiesen. Ein Aufruf nimmt 1 bis 4096 Fills, Seitenzahlen müssen existieren, und Ressourcennamen sind auf 127 Zeichen aus Buchstaben, Ziffern, Unterstrich, Minus und Punkt begrenzt. Verschlüsselte Eingabe wird an der Trailer-Stufe abgelehnt. Ein signiertes Dokument braucht AllowSignedDocument := True, und da die Ausgabe ein vollständiges Umschreiben statt eines inkrementellen Updates ist, validieren bestehende Signaturen nicht mehr — signieren Sie also nach dem Ergänzen von Gradients. Jeder Fill wird am Ende des Seiteninhalts gemalt, auf dem, was bereits da ist; für einen Hintergrundeffekt erzeugen Sie den Gradient zuerst und ergänzen Text und Bilder danach, oder verwenden optionalen Inhalt wie im Artikel zum Steuern von PDF-Optional-Content-Layern in Delphi beschrieben, wenn die Füllung umschaltbar sein soll

FAQ

Unterstützt SaveAsShadingCreated Multi-Stop-Gradients?

Ja. Übergeben Sie 2 bis 256 Einträge in Stops mit streng von 0 bis 1 aufsteigenden Positionen. Zwei Stops produzieren eine Type-2-Function, drei oder mehr produzieren eine Type-3-Stitching-Function mit abgeleiteten /Bounds- und /Encode-Arrays

Warum ist mein Gradient in PDFium unsichtbar, obwohl die Datei gültig aussieht?

Prüfen Sie, dass /ShadingType der Integer 2 oder 3 statt eines Namens ist, und dass der sh-Operator innerhalb eines re W n-Clips im selben Content-Stream läuft. PDFium überspringt ungültige Shading-Typen still und verwirft Shading-Objekte mit leerem Rechteck

Welche Color Spaces kann ein erzeugtes Shading verwenden?

TPdfShadingCreate akzeptiert DeviceGray, DeviceRGB, DeviceCMYK, CalGray, CalRGB, Lab und ICCBased mit einem 1-, 3- oder 4-Komponenten-Profil. Pattern- und Indexed-Color-Spaces gehören nicht zum Erstellungsprofil

Gradient-Erstellung, Optionaler Content und der Rest der gestaffelten Writer kommen mit dem PDFium Component for Delphi and C++Builder, das PDFium für Delphi-, C++Builder- und Lazarus-Projekte umhüllt