Technisch artikel

Axiale en radiale PDF-gradients in Delphi met PDFium

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

Axiale en radiale shading-geometrie van PDFium Component voor Delphi: een Type 2-gradient mengt StartColor naar EndColor langs de lijn van x0 y0 naar x1 y1 met /Extend true true die voorbij beide einden doorloopt, terwijl een Type 3-gradient mengt tussen een startcirkel met straal r0 en een eindcirkel met straal r1
PDFium schildert de shading over wat de clip toelaat: Type 2 Coords plaatsen de mengas van x0 y0 naar x1 y1, Type 3 Coords plaatsen de twee cirkels, en /Extend [true true] draagt beide eindkleuren voorbij de geometrie
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

Compilatie van multi-stop gradients door PDFium Component in Delphi: drie Stops op posities 0.0, 0.4 en 1.0 worden een Type 3 stitching-functie met één Type 2-segment per aangrenzend stoppaar, /Bounds [0.4] voor de binnenste positie en /Encode [0 1 0 1] dat elk subdomein terugmapt op het volledige bereik
Elk aangrenzend stoppaar wordt één Type 2-segment binnen een Type 3-functie; de afgeleide arrays /Bounds en /Encode zijn wat handgeschreven stitching-functies doorgaans verkeerd doen

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

Waarom een geldig ogende PDF-shading niets tekent in PDFium: een /ShadingType geschreven als de naam Axial wordt door een integer-getter als 0 gelezen en de sh-operator wordt zwijgend overgeslagen, en een content stream met alleen sh heeft geen bounding box totdat een expliciete re W n-clip hem een rechthoek geeft
Beide onzichtbare-shading-faalmomenten zijn stil in PDFium, dus SaveAsShadingCreated schrijft zelf de integer ShadingType en wikkelt elke sh-aanroep in een crop-box re W n-clip

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