Introduction
PDF is not only about text data storage, but it also specifies a rich set of drawing operations making it possible to achieve very complex visual effects. In this article we’ll talk about its gradient filling and stroking capabilities. Using the API provided by Apitron PDF Kit for .NET component you’ll be able to create advanced color gradient fills based on standard linear and exponential interpolation functions as well as pure PostScript function-based color transition effects. In PDF, the object used to create a complex fill is a called a Shading, and you may read the low level details involved in article 8.7.4 Shading Patterns in PDF specification.
The Shading object requires a function object that defines the color transformation needed to create the final color at the each point where the shading is defined. Functions are described in section 7.10 Functions of the PDF specification. Using various function types it becomes possible to implement linear, bilinear and exponential interpolation as well as other color effects. You can use different functions to calculate the value for color components independently or at once. Currently available function types are: Sampled, Exponential, StitchingFunction and PostScript. Shading types available are: axial, radial and function-based.
When the area to be painted is a relatively simple shape whose geometry is the same as that of the gradient fill itself or you have a simple clipping path for it, the ClippedContent::PaintShading() function may be used instead of the usual painting approach. It accepts a Shadingobject as an operand and applies the corresponding gradient fill directly to current user space applying the additional clipping path given as a parameter if needed.
Another way to use the shading is to define a ShadingPattern. Similar to the TilingPattern object it can be used as a color for fill and stroke operations if you set the current stroking or non-stroking colorspace as PredefinedColorpaces::Pattern. This way you’ll be able to draw whatever you want using the shading as color provider for stroked or filled point.
Both methods have own pros and cons and it’s up to you to choose the one that works best it your situation. Read further to see various functions types and shadings types in action.
Axial Shading based on Exponential interpolation function
Here we define the axial shading object that does what it name says - defines a color blend along a line between two points, optionally extended beyond the boundary points by continuing the boundary colors. Exponential interpolation function is being used to calculate color transition. The formula used to produce each color sample is yj= C0j + xN× (C1j− C0j), and input and output values are limited by the domainand range intervals ([0,1] in this case).
// creates and registers axial shading object based on exponential function
privatestaticShading CreateAndRegisterAxialShadingBasedOnExponentialFunction(
FixedDocument doc, ColorbeginColor, Color endColor)
{
// exponential function producing the gradient
Function expFn = newExponentialFunction(Guid.NewGuid().ToString(),
beginColor.Components,
endColor.Components, 3, newdouble[] { 0, 1 });
// axial shading demonstrating exponential interpolation between two colors
AxialShading axialShadingExp = newAxialShading( Guid.NewGuid().ToString(),
PredefinedColorSpaces.RGB, newBoundary(0, 0, 180, 180), RgbColors.Green.Components,
newdouble[] { 0, 90, 180, 90 }, newstring[] { expFn.ID });
doc.ResourceManager.RegisterResource(expFn);
doc.ResourceManager.RegisterResource(axialShadingExp);
return axialShadingExp;
}
This shading is defined using rectangular boundary [0, 0,180,180] and two points which define the interpolation axis (0,90) and (180,90).
Axial Shading based on Sampled Function
The shading object created here is based on sampled function that uses SamplerDelegateto produce the color value for each calculated sample value. Samples count is the number of color passed as the parameter.
// creates and registers axial shading object based on sampled function able to interpolate
// between multiple colors
staticAxialShadingCreateAndRegisterAxialShadingBasedOnLinearInterpolationFunction(
FixedDocument doc, paramsobject[] colors)
{
int samplesCount = colors.Length;
double[] domain = newdouble[] {0, 1};
double step = domain[1] / (samplesCount-1);
// create the delegate returning color value for corresponding sample
SamplerDelegate fn = (double[] input) => {
int k = 0;
double tmpStep = step;
while (input[0] >= tmpStep)
{
tmpStep += step;
++k;
}
return (colors[k] asColor).Components;
};
// linear sampled function producing the gradient
Function linearFn = newSampledFunction(Guid.NewGuid().ToString(), fn, domain ,
newdouble[] { 0, 1, 0, 1, 0, 1 }, new[] { samplesCount }, BitsPerSample.OneByte);
// axial shading demonstrating linear interpolation between two colors
AxialShading axialShadingLinear = newAxialShading(Guid.NewGuid().ToString(),
PredefinedColorSpaces.RGB, newBoundary(0, 0, 180, 180), RgbColors.Green.Components,
newdouble[] { 0, 90, 180, 90 }, newstring[] { linearFn.ID });
doc.ResourceManager.RegisterResource(linearFn);
doc.ResourceManager.RegisterResource(axialShadingLinear);
return axialShadingLinear;
}
This shading is defined using rectangular boundary [0, 0,180,180] and two points which define the interpolation axis (0,90) and (180,90).
Function-based shading with PostScript function
In function-based shadings, the color at every point in the domain is defined by a specified mathematical function. The function need not be smooth or continuous. This type is the most general of the available shading types and is useful for shadings that cannot be adequately described with any of the other types. We used a separate PostScript function for each of the color components to produce the final color.
// creates and registers function-based shading object
privatestaticShading CreateAndRegisterFunctionBasedShading(FixedDocument doc)
{
// create post script functions for each color component
// it's possible to use only for one function and calculate all components at once
// the function used for R is 1- ((x-90)/90)^2 + ((y-90)/90)^2)
PostScriptFunction psFunctionR = newPostScriptFunction(Guid.NewGuid().ToString(),
newdouble[] {0, 180, 0, 180},
newdouble[] {0, 1},
"{90 sub 90 div dup mul exch 90 sub 90 div dup mul add 1 exch sub}");
// the function used for G is 1- ((x-90)/60)^2 + ((y-90)/60)^2)
PostScriptFunction psFunctionG = newPostScriptFunction(Guid.NewGuid().ToString(),
newdouble[] {0, 180, 0, 180},
newdouble[] {0, 1},
"{90 sub 60 div dup mul exch 90 sub 60 div dup mul add 1 exch sub}");
// the function used for G is 1- ((x-90)/30)^2 + ((y-90)/30)^2)
PostScriptFunction psFunctionB = newPostScriptFunction(Guid.NewGuid().ToString(),
newdouble[] {0, 180, 0, 180},
newdouble[] {0, 1},
"{90 sub 30 div dup mul exch 90 sub 30 div dup mul add 1 exch sub}");
// create shading based on three functions
FunctionShading functionShading = newFunctionShading(Guid.NewGuid().ToString(),
PredefinedColorSpaces.RGB,
newBoundary(180, 180), RgbColors.Green.Components,
newstring[] {psFunctionR.ID, psFunctionG.ID, psFunctionB.ID});
// register functions and shading object
doc.ResourceManager.RegisterResource(psFunctionR);
doc.ResourceManager.RegisterResource(psFunctionG);
doc.ResourceManager.RegisterResource(psFunctionB);
doc.ResourceManager.RegisterResource(functionShading);
return functionShading;
}
The domain for each function is the same as shading’s boundary, so the shading’s color is defined within each point of it.
Radial shading based on PostScript function
Radial shadings define a color blend that varies between two circles. Shadings of this type are commonly used to depict three-dimensional spheres and cones. Here we used a simple PostScript function that produces the output color using the simple formula: (1-x) for the red component, and zeros for others.
// creates and registers radial shading object
privatestaticShading CreateAndRegisterRadialShading(FixedDocument doc)
{
// create simple PS function that returns the following RGB color value: [(1-x), 0, 0]
PostScriptFunction psFunction = newPostScriptFunction(Guid.NewGuid().ToString(),
newdouble[]{0,1},newdouble[]{0,1,0,1,0,1}, "{1 exch sub 0 0}");
RadialShading radialShading = newRadialShading( Guid.NewGuid().ToString(),
PredefinedColorSpaces.RGB,newBoundary(180,180), RgbColors.White.Components,
newdouble[]{120,110,0,90,90,90},newstring[]{psFunction.ID});
// register function and shading
doc.ResourceManager.RegisterResource(psFunction);
doc.ResourceManager.RegisterResource(radialShading);
return radialShading;
}
This shading is defined using rectangular boundary [0, 0,180,180] and two circles defined by their centers and radii.
Shading Pattern
The shading pattern is like any other pattern except it’s based on shading, see 8.7 Patterns section of the PDF specification for the details. Thinking abstractly, the pattern is a like a color, depending on the location you fill or stoke in. In order to use it, you have to set the current stroking or non-stroking colorspace to a special value returned by the PredefinedColorspaces::Pattern property. We can use any shading as a base for the shading pattern. See the code below:
// create shading object
Shading functionShading = CreateAndRegisterFunctionBasedShading(doc);
// register shading pattern based on existing function-based shading
ShadingPattern shadingPattern = newShadingPattern(Guid.NewGuid().ToString(), functionShading.ID);
doc.ResourceManager.RegisterResource(shadingPattern);
We used the shading created by one of the functions described earlier, created ShadingPattern object based on it and registered it as a document’s resource. After that we can start using this pattern. Let’s create a FormXObject and use the pattern as a text fill.
// creates a reusable piece of content (FormXObject)
privatestaticFixedContent CreateAndRegisterXObject(FixedDocument doc,
ShadingPattern fillColor)
{
ClippedContent content =
newClippedContent(Path.CreateRoundRect(0,0,180,180,10,10,10,10));
content.SaveGraphicsState();
// draw gray rect as a background for our text
content.SetDeviceNonStrokingColor(RgbColors.LightGray.Components);
content.FillPath(Path.CreateRect(0, 0, 180, 180));
// draw text using shading pattern as a fill
content.Translate(3, 85);
TextObject txt = newTextObject(StandardFonts.Helvetica, 25);
// set the fill colorspace to Pattern in order to use the pattern as a color
txt.SetNonStrokingColorSpace(PredefinedColorSpaces.Pattern);
// set our pattern as a fill color
txt.SetNonStrokingColor(fillColor.ID);
txt.AppendText("Shading pattern");
content.AppendText(txt);
content.RestoreGraphicsState();
FixedContent formXObject = newFixedContent(Guid.NewGuid().ToString(),
newBoundary(180, 180), content);
doc.ResourceManager.RegisterResource(formXObject);
return formXObject;
}
And draw it:
// draw the sample FormXObject demonstrating text fill using shading pattern
firstPage.Content.AppendXObject(CreateAndRegisterXObject(doc,shadingPattern).ID, 390, 460);
Sample program (shadings)
The code below draws all shadings described above on one page, including shading pattern.
staticvoid Main(string[] args)
{
// create document object
FixedDocument doc = newFixedDocument();
// create shading objects
Shading axialShadingExp = CreateAndRegisterAxialShadingBasedOnExponentialFunction(doc,
RgbColors.Red, RgbColors.Black);
Shading axialShadingLinear =
CreateAndRegisterAxialShadingBasedOnLinearInterpolationFunction(doc,
newobject[] { RgbColors.Red, RgbColors.Black });
Shading axialShadingLinearMultipleColors =
CreateAndRegisterAxialShadingBasedOnLinearInterpolationFunction( doc,
newobject[] { RgbColors.Red, RgbColors.Green, RgbColors.Blue, RgbColors.Black });
Shading radialShading = CreateAndRegisterRadialShading(doc);
Shading functionShading = CreateAndRegisterFunctionBasedShading(doc);
// register shading pattern based on existing function-based shading
ShadingPattern shadingPattern = newShadingPattern(Guid.NewGuid().ToString(),
functionShading.ID);
doc.ResourceManager.RegisterResource(shadingPattern);
// create document page
Page firstPage = newPage();
// draw shadings
DrawShading(firstPage, axialShadingExp.ID, 10, 650);
DrawShading(firstPage, axialShadingLinear.ID, 200, 650);
DrawShading(firstPage, axialShadingLinearMultipleColors.ID, 390, 650);
DrawShading(firstPage, functionShading.ID, 10, 460);
DrawShading(firstPage, radialShading.ID, 200, 460);
// draw the sample xObject demonstrating text fill using shading pattern
firstPage.Content.AppendXObject(CreateAndRegisterXObject(doc,shadingPattern).ID, 390,
460);
// add page to document
doc.Pages.Add(firstPage);
// save document
using (Stream stream = File.Create("out.pdf"))
{
doc.Save(stream);
}
Process.Start("out.pdf");
}
// draws shading object on page at the specified coordinates
privatestaticvoid DrawShading(Page page, string resourceName, double xOffset,
double yOffset)
{
page.Content.SaveGraphicsState();
page.Content.Translate(xOffset, yOffset);
page.Content.PaintShading(resourceName, Path.CreateRoundRect(0,0,180,180,10,10,10,10));
page.Content.RestoreGraphicsState();
}
The image below demonstrates the created PDF document:
![]() |
Pic. 1 Shadings and shading patterns |
The complete code sample can be found in our githubrepo.
Conclusion
Using Apitron PDF Kit for .NET you can create advanced PDF export tools, for example modules for CAD software, graphical editors, or document export routines. Its API is flexible enough, and allows you to implement complex visual effects using graphical operations described in the PDF specification.
