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