Teknisk artikel

Axiala och radiella PDF-gradianter i Delphi med PDFium

PDFium Component skapar axiala och radiella PDF-gradianter i Delphi via TPdf.SaveAsShadingCreated. Du beskriver varje gradient i en TPdfShadingCreate-post (typ, färgutrymme, koordinater, färger eller stopp, målsida), och metoden skriver en verifierad kopia där varje fyllning är en äkta ISO 32000-1 shading-ordbok, registrerad i sidans /Resources /Shading-ordbok och målad av operatorn sh, så att PDFium renderar den nativt

Behovet dyker upp i rapport- och certifikatgeneratorer. En formgivare lämnar över en mockup med ett mjukt blått-till-vitt-sidhuvudband eller en radiell glöd bakom ett sigill, och sidobjekts-API:et som täcker text, banor och bilder har ingenting för det. Du kan fejka en gradient med hundra tunna rektanglar i något olika färger, men resultatet bandar synligt vid inzoomning, blåser upp innehållsströmmen och skriver ut dåligt. PDF har haft mjuka shadings sedan 1.3; arbetet ligger i att skriva dem så att en renderare faktiskt ritar dem

Varför skriver PDFium Component gradianter som ett separat sparande?

TPdf.SaveAsShadingCreated är en sparoperation snarare än ett sidobjektsanrop eftersom PDFium:s publika ABI saknar en funktion som skapar en shading-ordbok eller sänder ut en sh-operator. Sidobjektslagret som används vid att skapa PDF:er från grunden med PDFium Component bygger banor, text och bilder, och där stannar det. Shadings bor en nivå lägre, i objektgrafen och innehållsströmmens syntax

Så metoden arbetar i steg. PDFium sparar först det öppna dokumentet nativt, vilket behåller varje redigering du gjort via det normala API:et. PDFium Component skriver sedan om det mellanlagrade resultatet: den allokerar ett indirekt objekt per shading, lägger till resursposten på varje målsida, fogar en ny innehållsström till sidans /Contents, och skriver en färsk korsreferenstabell. Innan något når din ström eller fil laddas resultatet om med en strikt korsreferenskontroll, och metoden returnerar False med ett meddelande i rapporten om den omläsningen misslyckas. Filoverloaden skriver till en temporär fil först, så ett misslyckat sparande förstör inte ett befintligt mål

Att bygga en tvåfärgad axial gradient

En axial shading (ISO 32000-1 §8.7.4.5.3, Type 2) blandar färger längs en linje från x0 y0 till x1 y1, så Coords tar fyra värden. En radiell shading (§8.7.4.5.4, Type 3) blandar mellan två cirklar och tar sex: x0 y0 r0 x1 y1 r1. Med vanliga StartColor och EndColor sänder PDFium Component ut en Type 2-exponentialfunktion (§7.10.3) med /N 1, vilket är en rak linjär interpolation, och sätter /Extend [true true] så att slutfärgerna fortsätter förbi båda ändarna av axeln

Axial och radiell shadinggeometri i PDFium Component för Delphi: en Type 2-gradient blandar StartColor till EndColor längs linjen från x0 y0 till x1 y1 med /Extend true true som fortsätter förbi båda ändarna, medan en Type 3-gradient blandar mellan en startcirkel med radien r0 och en slutcirkel med radien r1
PDFium målar shadingen över vad klippet tillåter: Type 2-Coords placerar blandningsaxeln från x0 y0 till x1 y1, Type 3-Coords placerar de två cirklarna, och /Extend [true true] bär båda ändfärgerna förbi geometrin
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, uppifrån och ner
    Fills[0].StartColor := [0.12, 0.35, 0.75];  // mörkblå vid överkanten
    Fills[0].EndColor := [1, 1, 1];             // vitt längre ner
    Fills[0].PageNumber := 1;                   // 1-baserat
    Fills[0].ResourceName := 'HeaderBand';      // valfritt, standard är Sh1, Sh2...

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

Komponentantalet hos varje färg måste matcha färgutrymmet: en för pscDeviceGray och pscCalGray, tre för pscDeviceRGB, pscCalRGB och pscLab, fyra för pscDeviceCMYK. För pscICCBased kommer antalet från profilbytena du skickar i IccProfile, som måste exponera 1, 3 eller 4 komponenter; profilen bäddas in som sin egen ICC-ström med matchande /N. Kalibrerade utrymmen accepterar valfria CalWhitePoint (tre värden, positivt Y), CalGamma (ett värde för CalGray, tre för CalRGB) och, för Lab, en sexvärdes LabRange. Varje koordinat och komponent kontrolleras för NaN och oändlighet innan en byte skrivs

Notera att koordinaterna ligger i standardanvändarutrymme, med origo i sidans nedre vänstra hörn. Kom ihåg att axeln bara definierar var blandningen sker: det målade området är vad klippet tillåter, vilket spelar större roll än det låter, som nästa avsnitt visar

Flerstopps-gradianter kompileras till en Type 3-stitching-funktion

PDFium Component gör om en Stops-array till funktionen en PDF-läsare förväntar sig: två stopp blir en enda Type 2-funktion, och 3 till 256 stopp blir en Type 3-stitching-funktion (ISO 32000-1 §7.10.4) byggd av ett linjärt Type 2-segment per intilliggande par. Varje TPdfShadingCreateStop bär en Position och en Color. När Stops inte är tomt ersätter det StartColor och EndColor helt

Stitching-funktionen behöver tre arrayer som är lätta att få subtilt fel för hand. /Functions håller segmenten, /Bounds håller bara de inre stoppositionerna (k segment behöver k-1-gränser), och /Encode mappar varje subdomän tillbaka till [0 1] så att varje segment ser hela sitt indatavidd. Komponenten härleder alla tre från dina stopp, vilket är varför den är strikt mot indatan: positioner måste vara ändliga, strikt stigande, börja på 0 och sluta på 1 (inom 1e-6). En stopplista som börjar på 0.1 skulle lämna en del av domänen utan definierad funktion, och metoden avvisar den i stället för att gissa

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: en punkt i sigillcentrum som växer till 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;

För en trestopps-gradient ger det /Functions med två Type 2-segment, /Bounds [0.4] och /Encode [0 1 0 1]. Segmentgränserna är hårda färgsammanfogningar, så en stopplista jämnar inte ut över dem; ser en sammanfogning för skarp ut, lägg till stopp i stället för att förvänta dig att läsaren interpolerar krökning

Kompilering av flerstopps-gradient i PDFium Component i Delphi: tre Stops på positionerna 0.0, 0.4 och 1.0 blir en Type 3-stitching-funktion med ett Type 2-segment per intilliggande stopppar, /Bounds [0.4] för den inre positionen och /Encode [0 1 0 1] som mappar varje subdomän tillbaka till fullt intervall
Varje intilliggande stopppar blir ett Type 2-segment inne i en Type 3-funktion; de härledda /Bounds- och /Encode-arrayerna är vad handskrivna stitching-funktioner oftast får fel

Varför ritar PDFium tyst ingenting?

PDFium renderar en nyskapad shading bara när två detaljer är rätt: /ShadingType måste vara ett heltal, och sh-anropet måste sitta inne i en klippbana. Får du fel på den ena öppnas filen, validerar strukturellt och visar en orörd sida. Inget fel, ingen varning, ingen loggrad

Fälla ett: /ShadingType är ett tal, inte ett namn

ISO 32000-1 tabell 78 definierar /ShadingType som ett heltal: 2 för axial, 3 för radiell. Den första versionen av den här skrivaren sände ut /ShadingType /Axial eftersom det läste bättre i en hexdump. PDFium läser nyckeln med en heltalsgetter, får 0 för ett namn, klassar shadingen som ogiltig, misslyckas att läsa in den och hoppar över sh-operatorn utan klagan. Sidan renderades blank och ingenting i PDFium sa varför. Orsaken blev synlig först efter att en minimal handskriven PDF byggts och öppnats i en andra, oberoende renderare, som rapporterade en okänd shading-typ 0. Lärdomen generaliseras till varje PDF-skrivare: när specifikationen typar en uppräkning som heltal, skriv heltalet, hur frestande ett självbeskrivande namn än ser ut

Fälla två: en innehållsström som bara håller sh har ingen begränsningsruta

Operatorn sh (ISO 32000-1 §8.7.4.2) målar shadingen över det aktuella klippområdet, så sidans egen utsträckning begränsar den på papper. PDFium bygger dock ett shading-sidobjekt vars rektangel kommer antingen från den aktiva klippbanan eller från begränsningsrutan som ackumulerats av tidigare ritaoperatorer i samma ström, och renderaren gallrar objekt med en tom rektangel. En färsk tillfogad ström som inte innehåller något utöver /Sh1 sh har inget av dem, så gradienten kastas bort före rastreringen. TPdf.SaveAsShadingCreated läser därför varje sidas crop-ruta (med fall tillbaka på mediaboxen, sedan sidstorleken) och wrappar varje anrop i ett explicit re W n-klipp. På en US Letter-sida med origo i noll ser den tillfogade strömmen ut så här:

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

Paret q/Q isolerar klippet och eventuell transform från det befintliga sidinnehållet, så ingenting som ritas senare på sidan ärver dem. Skriver du shading-anrop själv i ett annat verktyg, kopiera den formen: ett explicit klipp i samma ström är billigt och tar bort beroendet av vad som kom före

Varför en gilltigsedande PDF-shading ritar ingenting i PDFium: en /ShadingType skriven som namnet Axial läses av en heltalsgetter som 0 och sh-operatorn hoppas över tyst, och en innehållsström som bara håller sh har ingen begränsningsruta förrän ett explicit re W n-klipp ger den en rektangel
Båda osynliga-shading-felarna är tysta i PDFium, så SaveAsShadingCreated skriver heltalet ShadingType själv och wrappar varje sh-anrop i ett crop-ruta re W n-klipp

Hur samspelar en anpassad gradientmatris med klippet?

En gradientmatris i PDFium Component appliceras som en cm-operator placerad efter klippet och före sh, så klippet förblir helsida medan bara gradientgeometrin transformeras. Sätt HasMatrix till True och fyll MatrixA till MatrixF, samma sex tal som vilken PDF-transformationsmatris som helst. Värdena måste vara ändliga och determinanten skild från noll, eftersom en singulär matris skulle kollapsa shadingen på en linje. Komponenten registrerar också de sex värdena som /Matrix i shading-ordboken, men för ett direkt sh-anrop är det cm:en i innehållsströmmen som sätter det koordinatsystem PDFium målar i

Ordningen spelar roll här, och det är den tredje fällan. Operanderna re tolkas i den aktuella transformationsmatrisen. Lägg cm:en först och den sidstor-klipprektangeln skalas, roteras eller skjuvas tillsammans med gradienten, så en matris som plattar till en ellips plattar också till det synliga området och delar av sidan tappar sin fyllning. Att klippa först i otransformerat användarutrymme och sedan konkatenera matrisen ger förväntat resultat: en transformerad gradient som täcker hela sidan. Guiden till Form XObject-stämplar och deras sexsiffriga matris går igenom vad varje matriskomponent gör om du behöver bygga en

procedure MakeEllipticalGlow(var Fill: TPdfShadingCreate);
begin
  BuildSealGlow(Fill);       // radiell fyllning centrerad på 450,160
  Fill.Coords := [0, 0, 0, 0, 0, 90];  // cirklar i origo
  Fill.HasMatrix := True;
  Fill.MatrixA := 1.6;       // sträck horisontellt
  Fill.MatrixB := 0;
  Fill.MatrixC := 0;
  Fill.MatrixD := 0.8;       // komprimera vertikalt
  Fill.MatrixE := 450;       // flytta sedan till sigillcentrum
  Fill.MatrixF := 160;
end;

Att läsa rapporten och känna gränserna

TPdfShadingCreateReport talar om vad som skrevs: SourceByteCount och OutputByteCount, CreatedShadingObjectNumbers (ett objektnummer per indatapost, i ordning), AttachedShadingCount, Succeeded och ErrorMessage. Valideringsfel namnger posten och fältet, till exempel en stopplista som inte är strikt stigande, så de är värda att logga ordagrant

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 är medvetet avgränsad. Den skapar bara axiala och radiella shadings; funktionsbaserade och mesh-shadings (Type 1 och 4 till 7) och mönsterfärgutrymmen avvisas. Ett anrop accepterar 1 till 4096 fyllningar, sidnummer måste finnas, och resursnamn begränsas till 127 tecken av bokstäver, siffror, understrykning, minus och punkt. Krypterad indata vägras vid trailersteget. Ett signerat dokument behöver AllowSignedDocument := True, och eftersom utdata är en fullständig omskrivning snarare än en inkrementell uppdatering kommer befintliga signaturer inte längre att validera, så signera efter att du lagt till gradienter. Varje fyllning målas i slutet av sidinnehållet, ovanpå det som redan finns där; för en bakgrundseffekt, skapa gradienten först och lägg till text och bilder efteråt, eller använd optional content som beskrivs i artikeln om att styra PDF optional content-lager i Delphi när fyllningen ska kunna växlas

FAQ

Stödjer SaveAsShadingCreated flerstopps-gradianter?

Ja. Skicka 2 till 256 poster i Stops med positioner strikt stigande från 0 till 1. Två stopp ger en Type 2-funktion och tre eller fler ger en Type 3-stitching-funktion med härledda /Bounds- och /Encode-arrayer

Varför är min gradient osynlig i PDFium när filen ser giltig ut?

Kontrollera att /ShadingType är heltalet 2 eller 3 i stället för ett namn, och att operatorn sh körs inne i ett re W n-klipp i samma innehållsström. PDFium hoppar tyst över ogiltiga shading-typer och gallrar shading-objekt med en tom rektangel

Vilka färgutrymmen kan en skapad shading använda?

TPdfShadingCreate accepterar DeviceGray, DeviceRGB, DeviceCMYK, CalGray, CalRGB, Lab och ICCBased med en 1-, 3- eller 4-komponentsprofil. Mönster- och Indexed-färgutrymmen är inte del av skapelseprofilen

Gradientsskapande, optional content och resten av de mellanlagrade skrivarna levereras med PDFium Component for Delphi and C++Builder, som wrappar PDFium för Delphi-, C++Builder- och Lazarus-projekt