Teknisk artikel

Axiale og radiale PDF-gradients i Delphi med PDFium

PDFium Component skaber axiale og radiale PDF-gradients i Delphi gennem TPdf.SaveAsShadingCreated. Du beskriver hver gradient i en TPdfShadingCreate-post (type, color space, koordinater, farver eller stops, målside), og metoden skriver en verificeret kopi, hvor hver fyld er en ægte ISO 32000-1 shading-dictionary, registreret i sidens /Resources /Shading-dictionary og malet af sh-operatoren, så PDFium renderer den nativt

Behovet viser sig i rapport- og certifikatgeneratorer. En designer afleverer en mockup med et blødt blåt-til-hvidt headerbånd eller en radial glød bag et segl, og page-object-API'en, der dækker tekst, stier og billeder, har intet til det. Du kan fake en gradient med hundrede tynde rektangler i let forskellige farver, men resultatet bander synligt ved indzoomning, oppuster content streamen og printer dårligt. PDF har haft bløde shadings siden 1.3; arbejdet består i at skrive dem, så en renderer faktisk tegner dem

Hvorfor skriver PDFium Component gradients som en separat gemning?

TPdf.SaveAsShadingCreated er en gem-operation snarere end et page-object-kald, fordi PDFium's offentlige ABI ingen funktion har, der skaber en shading-dictionary eller udsender en sh-operator. Det page-object-lag, der bruges ved at skabe PDF-filer fra bunden med PDFium Component, bygger stier, tekst og billeder, og dér stopper det. Shadings lever ét niveau lavere, i objektgrafen og content stream-syntaksen

Så metoden arbejder i stadier. PDFium gemmer først det åbne dokument nativt, hvilket bevarer hver redigering, du lavede gennem den normale API. PDFium Component omskriver derefter det scenariserede output: den allokerer ét indirekte objekt pr. shading, tilføjer resource-indgangen til hver målside, tilføjer en ny content stream til sidens /Contents og skriver en frisk cross-reference-tabel. Før noget når din stream eller fil, genindlæses resultatet med et strengt cross-reference-tjek, og metoden returnerer False med en besked i rapporten, hvis den genindlæsning fejler. Fil-overloaden skriver først til en midlertidig fil, så en mislykket gemning ikke ødelægger et eksisterende mål

At bygge en tofarvet axial gradient

En axial shading (ISO 32000-1 §8.7.4.5.3, Type 2) blander farver langs en linje fra x0 y0 til x1 y1, så Coords tager fire værdier. En radial shading (§8.7.4.5.4, Type 3) blander mellem to cirkler og tager seks: x0 y0 r0 x1 y1 r1. Med almindelige StartColor og EndColor udsender PDFium Component en Type 2 eksponentiel funktion (§7.10.3) med /N 1, hvilket er en lige linjeinterpolation, og sætter /Extend [true true], så slutfarverne fortsætter forbi begge ender af aksen

PDFium Component axial og radial shading-geometri til Delphi: en Type 2-gradient blander StartColor til EndColor langs linjen fra x0 y0 til x1 y1 med /Extend true true, der fortsætter forbi begge ender, mens en Type 3-gradient blander mellem en startcirkel med radius r0 og en slutcirkel med radius r1
PDFium maler shadingen hen over, hvad clip'et tillader: Type 2 Coords placerer blandingsaksen fra x0 y0 til x1 y1, Type 3 Coords placerer de to cirkler, og /Extend [true true] bærer begge slutfarver forbi geometrien
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, top til bund
    Fills[0].StartColor := [0.12, 0.35, 0.75];  // mørkeblå ved den øverste kant
    Fills[0].EndColor := [1, 1, 1];             // hvid længere nede
    Fills[0].PageNumber := 1;                   // 1-baseret
    Fills[0].ResourceName := 'HeaderBand';      // valgfri, default er Sh1, Sh2...

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

Komponentantallet for hver farve skal matche color space: én for pscDeviceGray og pscCalGray, tre for pscDeviceRGB, pscCalRGB og pscLab, fire for pscDeviceCMYK. For pscICCBased kommer antallet fra de profilbytes, du giver med i IccProfile, som skal eksponere 1, 3 eller 4 komponenter; profilen indlejres som sin egen ICC-stream med matchende /N. Kalibrerede spaces accepterer valgfri CalWhitePoint (tre værdier, positiv Y), CalGamma (én værdi for CalGray, tre for CalRGB) og, for Lab, en seksværdi LabRange. Hver koordinat og komponent tjekkes for NaN og uendelig, før en byte skrives

Bemærk, at koordinater er i default user space, med origo i sidens nederste venstre hjørne. Husk, at aksen kun definerer, hvor blandingen sker: det malede område er hvad end clip'et tillader, hvilket betyder mere, end det lyder, som de næste afsnit viser

Multi-stop-gradients kompilerer til en Type 3 stitching-funktion

PDFium Component omsætter et Stops-array til den funktion, en PDF-reader forventer: to stops bliver til én enkelt Type 2-funktion, og 3 til 256 stops bliver til en Type 3 stitching-funktion (ISO 32000-1 §7.10.4) bestående af ét lineært Type 2-segment pr. nabopar. Hver TPdfShadingCreateStop bærer en Position og en Color. Er Stops ikke tom, erstatter den StartColor og EndColor fuldstændig

Stitching-funktionen behøver tre arrays, der er lette at få subtilt forkert i hånden. /Functions rummer segmenterne, /Bounds rummer kun de indre stoppositioner (k segmenter behøver k-1 bounds), og /Encode mapper hvert subdomæne tilbage til [0 1], så hvert segment ser sit fulde inputinterval. Komponenten udleder alle tre af dine stops, hvilket er grunden til, at den er streng med inputtet: positioner skal være endelige, strengt stigende, starte ved 0 og ende ved 1 (inden for 1e-6). En stopliste, der starter ved 0,1, ville efterlade en del af domænet uden en defineret funktion, og metoden afviser den i stedet for at gætte

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: et punkt i seglets center voksende til en 90 pt-cirkel
  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;

For en tre-stop-gradient producerer det /Functions med to Type 2-segmenter, /Bounds [0.4] og /Encode [0 1 0 1]. Segmentgrænserne er hårde farvesamlinger, så en stopliste udjævner ikke hen over dem; ser en samling for skarp ud, så tilføj stops frem for at forvente, at readeren interpolerer krumning

PDFium Component multi-stop-gradient-kompilering i Delphi: tre Stops ved positionerne 0.0, 0.4 og 1.0 bliver til en Type 3 stitching-funktion med ét Type 2-segment pr. nabostop-par, /Bounds [0.4] for den indre position og /Encode [0 1 0 1], der mapper hvert subdomæne tilbage til fuldt interval
Hvert nabostop-par bliver til ét Type 2-segment inde i en Type 3-funktion; de udledte /Bounds- og /Encode-arrays er det, håndskrevne stitching-funktioner plejer at få forkert

Hvorfor tegner PDFium stille intet?

PDFium renderer en nyoprettet shading kun, når to detaljer er rigtige: /ShadingType skal være et heltal, og sh-kaldet skal ligge inde i en clipping path. Få bare én af dem forkert, og filen åbner, validerer strukturelt og viser en urørt side. Ingen fejl, ingen advarsel, ingen loglinje

Fælde én: /ShadingType er et tal, ikke et navn

ISO 32000-1 Tabel 78 definerer /ShadingType som et heltal: 2 for axial, 3 for radial. Den første version af denne writer udsendte /ShadingType /Axial, fordi det læste bedre i en hex-dump. PDFium læser nøglen med en integer-getter, får 0 for et navn, klassificerer shadingen som ugyldig, fejler at loade den og springer sh-operatoren over uden klage. Siden renderede blank, og intet i PDFium sagde hvorfor. Årsagen blev først synlig, efter at en minimal håndskrevet PDF blev bygget og åbnet i en anden, uafhængig renderer, som rapporterede en ukendt shadingtype på 0. Lektionen generaliserer til enhver PDF-writer: når specifikationen typer en enumeration som et heltal, så skriv heltallet, hvor fristende et selvbeskrivende navn end ser ud

Fælde to: en content stream med kun sh har ingen bounding box

sh-operatoren (ISO 32000-1 §8.7.4.2) maler shadingen hen over den aktuelle clipping-region, så sidens egen udstrækning afgrænser den på papir. PDFium bygger dog et shading page-objekt, hvis rektangel kommer enten fra den aktive clip-sti eller fra den bounding box, tidligere tegneoperatorer i samme stream har akkumuleret, og rendereren kuller objekter med et tomt rektangel. En frisk tilføjet stream, der ikke rummer andet end /Sh1 sh, har intet af det, så gradienten droppes før rasteriseringen. TPdf.SaveAsShadingCreated læser derfor hver sides crop box (med fallback til media box og derefter sidestørrelsen) og pakker hver invokation ind i et eksplicit re W n-clip. På en US Letter-side med origo ved nul ser den tilføjede stream sådan ud:

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

q/Q-parret isolerer clip'et og enhver transform fra det eksisterende sideindhold, så intet tegnet senere på siden arver dem. Skriver du shading-invokationer selv i et andet værktøj, så kopier denne form: et eksplicit clip i samme stream er billigt og fjerner afhængigheden af, hvad end der kom før

Hvorfor en gyldigt udseende PDF-shading tegner intet i PDFium: en /ShadingType skrevet som navnet Axial læses af en integer-getter som 0, og sh-operatoren springes stille over, og en content stream med kun sh har ingen bounding box, indtil et eksplicit re W n-clip giver den et rektangel
Begge usynlige-shading-fiaskoer er stille i PDFium, så SaveAsShadingCreated skriver integer ShadingType selv og pakker hvert sh-kald ind i et crop-box re W n-clip

Hvordan interagerer en custom gradientmatrix med clip'et?

En gradientmatrix i PDFium Component anvendes som en cm-operator placeret efter clip'et og før sh, så clip'et forbliver fuld side, mens kun gradientgeometrien transformeres. Sæt HasMatrix til True og udfyld MatrixA gennem MatrixF, de samme seks tal som enhver PDF-transformationmatrix. Værdierne skal være endelige, og determinanten ikke-nul, fordi en singulær matrix ville kollapse shadingen ned på en linje. Komponenten registrerer også de seks værdier som /Matrix i shading-dictionaryen, men ved et direkte sh-kald er det cm'en i content streamen, der sætter det koordinatsystem, PDFium maler i

Rækkefølgen betyder noget her, og det er tredje fælde. re-operanderne fortolkes i den aktuelle transformationmatrix. Sæt cm'en først, og sidestørrelses-clip-rektanglet skaleres, roteres eller skævvrides med gradienten, så en matrix, der flader en ellipse ud, også flader det synlige område ud, og dele af siden mister deres fyld. At clippe først i utransformeret user space og derefter konkatenerer matrixen giver det forventede resultat: en transformeret gradient, der dækker hele siden. Guiden til Form XObject-stempler og deres sekstalsmatrix gennemgår, hvad hver matrixkomponent gør, hvis du skal bygge én

procedure MakeEllipticalGlow(var Fill: TPdfShadingCreate);
begin
  BuildSealGlow(Fill);       // radial fyld centreret ved 450,160
  Fill.Coords := [0, 0, 0, 0, 0, 90];  // cirkler i origo
  Fill.HasMatrix := True;
  Fill.MatrixA := 1.6;       // stræk vandret
  Fill.MatrixB := 0;
  Fill.MatrixC := 0;
  Fill.MatrixD := 0.8;       // komprimér lodret
  Fill.MatrixE := 450;       // flyt derefter til seglets center
  Fill.MatrixF := 160;
end;

At læse rapporten og kende grænserne

TPdfShadingCreateReport fortæller dig, hvad der blev skrevet: SourceByteCount og OutputByteCount, CreatedShadingObjectNumbers (ét objektnummer pr. inputindgang, i rækkefølge), AttachedShadingCount, Succeeded og ErrorMessage. Valideringsfejl navngiver indgangen og feltet, for eksempel en stopliste, der ikke er strengt stigende, så de er værd at logge ordret

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

Profilen er bevidst afgrænset. Den skaber kun axiale og radiale shadings; funktionsbaserede og mesh-shadings (Type 1 og 4 til 7) og pattern color spaces afvises. Ét kald accepterer 1 til 4096 fyld, sidenumre skal eksistere, og resource-navne er begrænset til 127 tegn af bogstaver, cifre, underscore, minus og punktum. Krypteret input afvises ved trailer-stadiet. Et signeret dokument behøver AllowSignedDocument := True, og da outputtet er en fuldstændig omskrivning frem for en trinvis opdatering, validerer eksisterende signaturer ikke længere, så signér efter tilføjelse af gradients. Hvert fyld males til sidst i sideindholdet, oven på det, der allerede er der; til en baggrundseffekt, skab gradienten først og tilføj tekst og billeder bagefter, eller brug optional content som beskrevet i artiklen om at styre PDF optional content layers i Delphi, når fyldet skal kunne slås til og fra

FAQ

Understøtter SaveAsShadingCreated multi-stop-gradients?

Ja. Giv 2 til 256 indgange med i Stops med positioner strengt stigende fra 0 til 1. To stops producerer en Type 2-funktion, og tre eller flere producerer en Type 3 stitching-funktion med udledte /Bounds- og /Encode-arrays

Hvorfor er min gradient usynlig i PDFium, når filen ser gyldig ud?

Tjek at /ShadingType er heltallet 2 eller 3 frem for et navn, og at sh-operatoren kører inde i et re W n-clip i samme content stream. PDFium springer ugyldige shadingtyper stille over og kuller shading-objekter med et tomt rektangel

Hvilke color spaces kan en skabt shading bruge?

TPdfShadingCreate accepterer DeviceGray, DeviceRGB, DeviceCMYK, CalGray, CalRGB, Lab og ICCBased med en 1-, 3- eller 4-komponentprofil. Pattern- og Indexed color spaces er ikke en del af skabelsesprofilen

Gradientskabelse, optional content og resten af de scenariserede writers følger med PDFium Component for Delphi og C++Builder, som wrapper PDFium til Delphi-, C++Builder- og Lazarus-projekter