PDFium Component maakt axiale en radiale PDF-gradients in Delphi via TPdf.SaveAsShadingCreated. U beschrijft elke gradient in een record TPdfShadingCreate (type, color space, coördinaten, kleuren of stops, doelpagina), en de methode schrijft een geverifieerde kopie waarin elke vulling een echte ISO 32000-1 shading dictionary is, geregistreerd in de dictionary /Resources /Shading van de pagina en geschilderd door de operator sh, zodat PDFium hem native rendert
De behoefte verschijnt in rapport- en certificaatgenerators. Een ontwerper levert een mock-up aan met een zachte blauw-naar-wit headerband of een radiale gloed achter een zegel, en de page-object API die tekst, paths en images dekt heeft er niets voor. U kunt een gradient veinzen met honderd dunne rechthoeken in licht verschillende kleuren, maar het resultaat vertoont zichtbaar banden bij het inzoomen, zet de content stream op en print slecht. PDF heeft smooth shadings sinds 1.3; het werk zit in ze zo schrijven dat een renderer ze werkelijk tekent
Waarom schrijft PDFium Component gradients als aparte save?
TPdf.SaveAsShadingCreated is een save-operatie in plaats van een page-object-aanroep, want de publieke ABI van PDFium heeft geen functie die een shading dictionary aanmaakt of een operator sh uitzendt. De page-object-laag die wordt gebruikt bij PDF's maken vanaf nul met PDFium Component bouwt paths, tekst en images, en daar stopt het. Shadings wonen één niveau lager, in de objectgraaf en de content stream-syntax
De methode werkt dus in fasen. PDFium slaat eerst het open document native op, wat elke bewerking die u via de normale API deed vasthoudt. Daarna herschrijft PDFium Component die tussentijdse output: hij wijst één indirect object per shading toe, voegt de resource-entry aan elke doelpagina toe, plakt een nieuwe content stream aan de pagina-/Contents en schrijft een verse cross-reference tabel. Voordat er ook maar iets uw stream of bestand bereikt, wordt het resultaat herladen met een strenge cross-reference-controle, en geeft de methode False terug met een boodschap in het rapport als dat herladen faalt. De file-overload schrijft eerst naar een tijdelijk bestand, dus een mislukte save wist geen bestaand doel
Een tweetekige axiale gradient bouwen
Een axiale shading (ISO 32000-1 §8.7.4.5.3, Type 2) mengt kleuren langs een lijn van x0 y0 naar x1 y1, dus Coords neemt vier waarden aan. Een radiale shading (§8.7.4.5.4, Type 3) mengt tussen twee cirkels en neemt er zes: x0 y0 r0 x1 y1 r1. Met kale StartColor en EndColor zendt PDFium Component een Type 2 exponentiële functie (§7.10.3) uit met /N 1, wat een rechte lineaire interpolatie is, en zet hij /Extend [true true] zodat de eindkleuren voorbij beide einden van de as doorlopen
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, van boven naar beneden
Fills[0].StartColor := [0.12, 0.35, 0.75]; // donkerblauw aan de bovenrand
Fills[0].EndColor := [1, 1, 1]; // wit lagerop
Fills[0].PageNumber := 1; // 1-based
Fills[0].ResourceName := 'HeaderBand'; // optioneel, default is Sh1, Sh2...
if not Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
raise Exception.Create('Shading creation failed: ' + Report.ErrorMessage);
finally
Pdf.Free;
end;
end;
Het aantal componenten van elke kleur moet matchen met de color space: één voor pscDeviceGray en pscCalGray, drie voor pscDeviceRGB, pscCalRGB en pscLab, vier voor pscDeviceCMYK. Voor pscICCBased komt het aantal uit de profielbytes die u in IccProfile meegeeft, die 1, 3 of 4 componenten moet blootleggen; het profiel wordt ingebed als zijn eigen ICC-stream met de matchende /N. Gecalibreerde spaces accepteren optioneel CalWhitePoint (drie waarden, positieve Y), CalGamma (één waarde voor CalGray, drie voor CalRGB) en, voor Lab, een LabRange van zes waarden. Elke coördinaat en component wordt gecontroleerd op NaN en oneindig voordat er een byte wordt geschreven
Merk op dat coördinaten in default user space staan, met de oorsprong linksonder op de pagina. Bedenk dat de as alleen bepaalt waar de menging gebeurt: het geschilderde gebied is wat de clip toelaat, en dat telt zwaarder dan het klinkt, zoals de volgende secties laten zien
Gradients met meerdere stops compileren naar een Type 3 stitching-functie
PDFium Component maakt van een array Stops de functie die een PDF-reader verwacht: twee stops worden één Type 2-functie, en 3 tot 256 stops worden een Type 3 stitching-functie (ISO 32000-1 §7.10.4) opgebouwd uit één lineair Type 2-segment per aangrenzend paar. Elk TPdfShadingCreateStop draagt een Position en een Color. Is Stops niet leeg, dan vervangt hij StartColor en EndColor volledig
De stitching-functie heeft drie arrays nodig die met de hand subtiel verkeerd gaan. /Functions bevat de segmenten, /Bounds bevat alleen de binnenste stopposities (k segmenten hebben k-1 bounds nodig), en /Encode mapt elk subdomein terug naar [0 1] zodat elk segment zijn volledige invoerbereik ziet. De component leidt alle drie af uit uw stops, en daarom is hij streng over de invoer: posities moeten eindig zijn, strikt oplopend, beginnen bij 0 en eindigen op 1 (binnen 1e-6). Een stoplijst die bij 0.1 begint zou een deel van het domein zonder gedefinieerde functie laten, en de methode wijst hem af in plaats van te gokken
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: een punt in het zegelmiddel groeiend naar een cirkel van 90 pt
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;
Voor een gradient met drie stops levert dat /Functions op met twee Type 2-segmenten, /Bounds [0.4] en /Encode [0 1 0 1]. De segmentgrenzen zijn harde kleurverbindingen, dus een stoplijst strijkt er niet overheen; oogt een verbinding te scherp, voeg dan stops toe in plaats van te verwachten dat de reader kromming interpoleert
Waarom tekent PDFium zwijgend niets?
PDFium rendert een pas aangemaakte shading pas wanneer twee details kloppen: /ShadingType moet een integer zijn, en de aanroep sh moet binnen een clipping path zitten. Zit er één van de twee naast, dan opent het bestand, valideert het structureel, en toont het een onaangeroerde pagina. Geen fout, geen waarschuwing, geen logregel
Valkuil één: /ShadingType is een getal, geen naam
ISO 32000-1 Table 78 definieert /ShadingType als integer: 2 voor axiaal, 3 voor radiaal. De eerste versie van deze writer zond /ShadingType /Axial uit omdat dat beter leesbaar was in een hex dump. PDFium leest de sleutel met een integer-getter, krijgt 0 voor een naam, classificeert de shading als ongeldig, faalt hem te laden en slaat de operator sh over zonder klacht. De pagina rendert leeg en niets in PDFium zegt waarom. De oorzaak werd pas zichtbaar na het bouwen van een minimaal handgeschreven PDF en het openen ervan in een tweede, onafhankelijke renderer, die een onbekend shading type van 0 meldde. De les generaliseert naar elke PDF-writer: als de specificatie een enumeratie als integer typeert, schrijf de integer, hoe verleidelijk een zichzelf-beschrijvende naam er ook uitziet
Valkuil twee: een content stream met alleen sh heeft geen bounding box
De operator sh (ISO 32000-1 §8.7.4.2) schildert de shading over de huidige clipping region, dus de omvang van de pagina zelf begrenst hem op papier. PDFium bouwt echter een shading page object waarvan de rechthoek komt of uit het actieve clip path of uit de bounding box die eerdere tekenoperators in dezelfde stream hebben opgestapeld, en de renderer kapt objecten met een lege rechthoek weg. Een vers toegevoegde stream die niets anders bevat dan /Sh1 sh heeft geen van beide, dus de gradient wordt weggegooid vóór rasterisatie. TPdf.SaveAsShadingCreated leest daarom de crop box van elke pagina (met terugval op de media box, dan op de paginagrootte) en wikkelt elke aanroep in een expliciete re W n-clip. Op een US Letter-pagina met zijn oorsprong op nul ziet de toegevoegde stream er zo uit:
q
0 0 612 792 re W n
/HeaderBand sh
Q
Het paar q/Q isoleert de clip en eventuele transform van de bestaande pagina-inhoud, dus niets dat later op de pagina wordt getekend erft ze. Schrijft u shading-aanroepen zelf in een ander gereedschap, kopieer dan deze vorm: een expliciete clip in dezelfde stream is goedkoop en haalt de afhankelijkheid van wat er eerder kwam weg
Hoe werkt een custom gradientmatrix samen met de clip?
Een gradientmatrix in PDFium Component wordt toegepast als een operator cm geplaatst na de clip en vóór sh, dus de clip blijft paginabreed terwijl alleen de gradientgeometrie wordt getransformeerd. Zet HasMatrix op True en vul MatrixA tot en met MatrixF, dezelfde zes getallen als elke PDF-transformatiematrix. De waarden moeten eindig zijn en de determinant niet-nul, want een singuliere matrix zou de shading op een lijn doen instorten. De component legt de zes waarden ook vast als /Matrix in de shading dictionary, maar bij een directe aanroep sh is het de cm in de content stream die het coördinatensysteem zet waarin PDFium schildert
Volgorde telt hier, en dat is de derde valkuil. De operanden van re worden geïnterpreteerd in de huidige transformatiematrix. Zet de cm eerst, dan wordt de paginagrote cliprechthoek samen met de gradient geschaald, gedraaid of gescheerd, dus een matrix die een ellips platdrukt drukt ook het zichtbare gebied plat en delen van de pagina verliezen hun vulling. Eerst clippen in ongetransformeerde user space en daarna de matrix concateneren geeft het verwachte resultaat: een getransformeerde gradient die de hele pagina dekt. De gids voor Form XObject-stempels en hun matrix van zes getallen loopt langs wat elke matrixcomponent doet als u er een moet bouwen
procedure MakeEllipticalGlow(var Fill: TPdfShadingCreate);
begin
BuildSealGlow(Fill); // radiale vulling gecentreerd op 450,160
Fill.Coords := [0, 0, 0, 0, 0, 90]; // cirkels in de oorsprong
Fill.HasMatrix := True;
Fill.MatrixA := 1.6; // horizontaal uitrekken
Fill.MatrixB := 0;
Fill.MatrixC := 0;
Fill.MatrixD := 0.8; // verticaal samenpersen
Fill.MatrixE := 450; // daarna naar het zegelmiddel verplaatsen
Fill.MatrixF := 160;
end;
Het rapport lezen en de grenzen kennen
TPdfShadingCreateReport vertelt u wat er is geschreven: SourceByteCount en OutputByteCount, CreatedShadingObjectNumbers (één objectnummer per invoerentry, in volgorde), AttachedShadingCount, Succeeded en ErrorMessage. Validatiefouten benoemen de entry en het veld, bijvoorbeeld een stoplijst die niet strikt oploopt, dus ze zijn het letterlijk loggen waard
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);
Het profiel is bewust begrensd. Hij maakt alleen axiale en radiale shadings aan; function-based en mesh shadings (Types 1 en 4 tot 7) en pattern color spaces worden geweigerd. Eén aanroep accepteert 1 tot 4096 vullingen, paginanummers moeten bestaan, en resourcenamen zijn beperkt tot 127 tekens van letters, cijfers, underscore, min en punt. Versleutelde invoer wordt in de trailerfase geweigerd. Een ondertekend document heeft AllowSignedDocument := True nodig, en omdat de output een volledige herschrijving is in plaats van een incrementele update, valideren bestaande handtekeningen niet meer, dus teken na het toevoegen van gradients. Elke vulling wordt aan het einde van de pagina-inhoud geschilderd, bovenop wat er al staat; voor een achtereffect maakt u eerst de gradient aan en voegt u daarna tekst en images toe, of gebruikt u optional content zoals beschreven in het artikel over PDF optional content-layers regelen in Delphi wanneer de vulling schakelbaar moet zijn
FAQ
Ondersteunt SaveAsShadingCreated gradients met meerdere stops?
Ja. Geef 2 tot 256 entries mee in Stops met posities strikt oplopend van 0 tot 1. Twee stops leveren een Type 2-functie op en drie of meer een Type 3 stitching-functie met afgeleide arrays /Bounds en /Encode
Waarom is mijn gradient onzichtbaar in PDFium terwijl het bestand geldig lijkt?
Controleer dat /ShadingType de integer 2 of 3 is in plaats van een naam, en dat de operator sh binnen een re W n-clip in dezelfde content stream draait. PDFium slaat ongeldige shading types zwijgend over en kapt shading objecten met een lege rechthoek weg
Welke color spaces kan een aangemaakte shading gebruiken?
TPdfShadingCreate accepteert DeviceGray, DeviceRGB, DeviceCMYK, CalGray, CalRGB, Lab en ICCBased met een profiel van 1, 3 of 4 componenten. Pattern- en Indexed color spaces maken geen deel uit van het aanmaakprofiel
Gradient-aanmaak, optional content en de rest van de staged writers worden geleverd met de PDFium Component for Delphi and C++Builder, die PDFium omwikkelt voor Delphi-, C++Builder- en Lazarus-projecten