Skip to main content

Gradients

A gradient brush blends between two colours. XLinearGradientBrush blends along a line, and XRadialGradientBrush blends outwards from one circle to another. Use a gradient brush anywhere a brush goes: to fill a shape, a path or text, or as the brush of an XPen. Both brushes are in the core PdfPinata package and need no backend.

A gradient is written into the PDF as a shading pattern: type 2 (axial) for a linear gradient and type 3 (radial) for a radial one. A reader draws the blend itself, so it stays smooth at every zoom.

Radial gradients

XRadialGradientBrush takes a centre, two radii and two colours. The first colour is on the circle with the first radius, and the second colour is on the circle with the second radius. With a first radius of 0, the first colour is a point at the centre:

src/SampleApp/Demos/GradientsDemo.cs
Panel(0, 0, "One centre", r =>
{
// The first colour at the first radius, the second at the second. With a first radius
// of zero the first colour is a point at the centre.
var brush = new XRadialGradientBrush(Middle(r), 0, 55, XColors.Gold, XColors.Firebrick);
gfx.DrawEllipse(brush, Middle(r).X - 55, Middle(r).Y - 55, 110, 110);
});

The first radius can be larger than the second. The blend then runs inwards.

Two centres

The other constructor takes a separate centre for each circle. Put the first circle, a point, off the centre of the second to make a highlight, as on a lit sphere:

src/SampleApp/Demos/GradientsDemo.cs
Panel(0, 1, "Two centres", r =>
{
// The first circle is a point up and to the left of the second circle's centre, so the
// highlight sits off-centre, as on a lit sphere.
var centre = Middle(r);
var highlight = new XPoint(centre.X - 18, centre.Y - 18);
var brush = new XRadialGradientBrush(highlight, centre, 0, 55, XColors.White, XColors.DarkGreen);
gfx.DrawEllipse(brush, centre.X - 55, centre.Y - 55, 110, 110);
});

Keep the first circle inside the second. If it is not, the shape of the blend becomes a cone, which is correct PDF but seldom what you want.

Extending past the ends

A gradient paints nothing before its start or after its end. For a radial gradient, the start is the first circle and the end is the second circle. So a radial gradient that fills a rectangle leaves the corners outside its outer circle unpainted. Set ExtendRight to paint them in the second colour:

src/SampleApp/Demos/GradientsDemo.cs
Panel(1, 0, "ExtendRight", r =>
{
// Nothing is painted beyond the outer circle unless ExtendRight says so. Here the
// rectangle is larger than the circle, and its corners take the outer colour.
var brush = new XRadialGradientBrush(Middle(r), 0, 50, XColors.White, XColors.SteelBlue)
{
ExtendRight = true
};
gfx.DrawRectangle(brush, r);
});

ExtendLeft does the same at the start. A first radius above 0 leaves a hole inside the first circle, and ExtendLeft fills it with the first colour:

src/SampleApp/Demos/GradientsDemo.cs
Panel(1, 1, "ExtendLeft", r =>
{
// A first radius above zero leaves a hole inside the first circle. ExtendLeft fills
// it with the first colour.
var brush = new XRadialGradientBrush(Middle(r), 25, 55, XColors.Orange, XColors.Indigo)
{
ExtendLeft = true
};
gfx.DrawEllipse(brush, Middle(r).X - 55, Middle(r).Y - 55, 110, 110);
});

Both properties are on XLinearGradientBrush too. There, the start is the first point and the end is the second point:

src/SampleApp/Demos/GradientsDemo.cs
Panel(0, 2, "Linear, extended", r =>
{
// The blend runs across the middle third only. Extended at both ends, the rest of the
// rectangle takes the colour of the nearer end.
var start = new XPoint(r.X + r.Width / 3, r.Y);
var end = new XPoint(r.X + 2 * r.Width / 3, r.Y);
var brush = new XLinearGradientBrush(start, end, XColors.Teal, XColors.Coral)
{
ExtendLeft = true,
ExtendRight = true
};
gfx.DrawRectangle(brush, r);
});

Both properties are false by default, and they have the same names as in PDFsharp.

Transforms

The gradient's points and radii are in the same coordinates as the shape you fill. The transform of the XGraphics applies to the brush in the same way as to the shape, so a gradient drawn under a ScaleTransform or a RotateTransform scales and turns with its shape.

A gradient brush also has a transform of its own. Set Transform, or call TranslateTransform, ScaleTransform, RotateTransform or MultiplyTransform on the brush. This transform applies to the brush alone, before the transform of the XGraphics. Use it to stretch the circles of a radial gradient into ellipses:

src/SampleApp/Demos/GradientsDemo.cs
Panel(0, 3, "Radial, stretched by Transform", r =>
{
// A circle about the origin, made twice as wide as it is tall, then moved to the
// middle of the panel. The gradient's rings become ellipses.
var brush = new XRadialGradientBrush(new XPoint(0, 0), 0, 30, XColors.Yellow, XColors.Purple)
{
ExtendRight = true
};
brush.Transform = new XMatrix(2, 0, 0, 1, Middle(r).X, Middle(r).Y);
gfx.DrawRectangle(brush, r);
});

or to turn a linear gradient without calculating new end points:

src/SampleApp/Demos/GradientsDemo.cs
Panel(2, 2, "Linear, turned by Transform", r =>
{
// Left to right across the panel, then turned by 30 degrees about the panel's middle.
// Turned, the axis no longer reaches two of the corners, so both ends are extended.
var brush = new XLinearGradientBrush(new XPoint(r.X, r.Y), new XPoint(r.Right, r.Y),
XColors.Navy, XColors.LightSkyBlue)
{
ExtendLeft = true,
ExtendRight = true
};
brush.RotateTransform(30);
brush.TranslateTransform(-Middle(r).X, -Middle(r).Y, XMatrixOrder.Prepend);
brush.TranslateTransform(Middle(r).X, Middle(r).Y, XMatrixOrder.Append);
gfx.DrawRectangle(brush, r);
});

A brush's rotation turns about the origin, not about the shape. To turn a brush about a point, move that point to the origin first and back afterwards, as the example does.

Transparency

Either colour of a gradient can have an alpha. The gradient then blends from one transparency to the other, as well as from one colour to the other:

src/SampleApp/Demos/GradientsDemo.cs
Panel(1, 3, "Fading to transparent", r =>
{
for (var x = r.X; x < r.Right; x += 12)
gfx.DrawRectangle(XBrushes.LightGray, x, r.Y, 6, r.Height);

// A transparent second colour lets the stripes show through towards the rim.
var brush = new XRadialGradientBrush(Middle(r), 0, 55,
XColors.Crimson, XColor.FromArgb(0, XColors.Crimson));
gfx.DrawRectangle(brush, r);
});

A gradient with transparency is drawn through a soft mask, which PDF/A-1 does not allow. See PDF/A.

Things to know

  • Only two colours. A gradient blends between two colours. For more colour stops, draw several gradients next to each other, or nest radial gradients with matching radii.
  • Unpainted means unpainted. Without ExtendLeft or ExtendRight, the parts of the shape past the gradient's ends show what was on the page before, not white.
  • The same brush can fill many shapes. The gradient's coordinates are the coordinates you draw in, not coordinates relative to the shape. Two rectangles filled with the same brush show two parts of one gradient.

See it in action

The Gradients demo draws every gradient on this page side by side, with and without extension. Shapes, pens and brushes covers the other brushes and the shapes they fill.

The full Gradients demo
src/SampleApp/Demos/GradientsDemo.cs
var document = new PdfDocument();
document.Info.Title = "Gradients";

var heading = new XFont("Liberation Sans", 16, XFontStyle.Bold);
var label = new XFont("Liberation Sans", 8);

var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);
gfx.DrawString("Gradients", heading, XBrushes.Black, new XPoint(50, 60));

// Each panel is a titled box in a three-by-four grid, and the lambda draws inside it.
void Panel(int column, int row, string title, Action<XRect> draw)
{
var cell = new XRect(50 + column * 165, 90 + row * 170, 155, 160);
gfx.DrawRectangle(new XPen(XColors.Gainsboro, 0.5), cell);
draw(new XRect(cell.X + 8, cell.Y + 8, cell.Width - 16, cell.Height - 30));
gfx.DrawString(title, label, XBrushes.Black,
new XRect(cell.X, cell.Bottom - 20, cell.Width, 14), XStringFormats.Center);
}

XPoint Middle(XRect r) => new(r.X + r.Width / 2, r.Y + r.Height / 2);

// ----- radial gradients -----

Panel(0, 0, "One centre", r =>
{
// The first colour at the first radius, the second at the second. With a first radius
// of zero the first colour is a point at the centre.
var brush = new XRadialGradientBrush(Middle(r), 0, 55, XColors.Gold, XColors.Firebrick);
gfx.DrawEllipse(brush, Middle(r).X - 55, Middle(r).Y - 55, 110, 110);
});

Panel(1, 0, "ExtendRight", r =>
{
// Nothing is painted beyond the outer circle unless ExtendRight says so. Here the
// rectangle is larger than the circle, and its corners take the outer colour.
var brush = new XRadialGradientBrush(Middle(r), 0, 50, XColors.White, XColors.SteelBlue)
{
ExtendRight = true
};
gfx.DrawRectangle(brush, r);
});

Panel(2, 0, "No extend", r =>
{
// The same brush without ExtendRight: the corners are left as they were.
var brush = new XRadialGradientBrush(Middle(r), 0, 50, XColors.White, XColors.SteelBlue);
gfx.DrawRectangle(brush, r);
});

Panel(0, 1, "Two centres", r =>
{
// The first circle is a point up and to the left of the second circle's centre, so the
// highlight sits off-centre, as on a lit sphere.
var centre = Middle(r);
var highlight = new XPoint(centre.X - 18, centre.Y - 18);
var brush = new XRadialGradientBrush(highlight, centre, 0, 55, XColors.White, XColors.DarkGreen);
gfx.DrawEllipse(brush, centre.X - 55, centre.Y - 55, 110, 110);
});

Panel(1, 1, "ExtendLeft", r =>
{
// A first radius above zero leaves a hole inside the first circle. ExtendLeft fills
// it with the first colour.
var brush = new XRadialGradientBrush(Middle(r), 25, 55, XColors.Orange, XColors.Indigo)
{
ExtendLeft = true
};
gfx.DrawEllipse(brush, Middle(r).X - 55, Middle(r).Y - 55, 110, 110);
});

Panel(2, 1, "A ring, no ExtendLeft", r =>
{
var brush = new XRadialGradientBrush(Middle(r), 25, 55, XColors.Orange, XColors.Indigo);
gfx.DrawEllipse(brush, Middle(r).X - 55, Middle(r).Y - 55, 110, 110);
});

// ----- linear gradients -----

Panel(0, 2, "Linear, extended", r =>
{
// The blend runs across the middle third only. Extended at both ends, the rest of the
// rectangle takes the colour of the nearer end.
var start = new XPoint(r.X + r.Width / 3, r.Y);
var end = new XPoint(r.X + 2 * r.Width / 3, r.Y);
var brush = new XLinearGradientBrush(start, end, XColors.Teal, XColors.Coral)
{
ExtendLeft = true,
ExtendRight = true
};
gfx.DrawRectangle(brush, r);
});

Panel(1, 2, "Linear, not extended", r =>
{
var start = new XPoint(r.X + r.Width / 3, r.Y);
var end = new XPoint(r.X + 2 * r.Width / 3, r.Y);
gfx.DrawRectangle(new XLinearGradientBrush(start, end, XColors.Teal, XColors.Coral), r);
});

Panel(2, 2, "Linear, turned by Transform", r =>
{
// Left to right across the panel, then turned by 30 degrees about the panel's middle.
// Turned, the axis no longer reaches two of the corners, so both ends are extended.
var brush = new XLinearGradientBrush(new XPoint(r.X, r.Y), new XPoint(r.Right, r.Y),
XColors.Navy, XColors.LightSkyBlue)
{
ExtendLeft = true,
ExtendRight = true
};
brush.RotateTransform(30);
brush.TranslateTransform(-Middle(r).X, -Middle(r).Y, XMatrixOrder.Prepend);
brush.TranslateTransform(Middle(r).X, Middle(r).Y, XMatrixOrder.Append);
gfx.DrawRectangle(brush, r);
});

// ----- transforms and transparency -----

Panel(0, 3, "Radial, stretched by Transform", r =>
{
// A circle about the origin, made twice as wide as it is tall, then moved to the
// middle of the panel. The gradient's rings become ellipses.
var brush = new XRadialGradientBrush(new XPoint(0, 0), 0, 30, XColors.Yellow, XColors.Purple)
{
ExtendRight = true
};
brush.Transform = new XMatrix(2, 0, 0, 1, Middle(r).X, Middle(r).Y);
gfx.DrawRectangle(brush, r);
});

Panel(1, 3, "Fading to transparent", r =>
{
for (var x = r.X; x < r.Right; x += 12)
gfx.DrawRectangle(XBrushes.LightGray, x, r.Y, 6, r.Height);

// A transparent second colour lets the stripes show through towards the rim.
var brush = new XRadialGradientBrush(Middle(r), 0, 55,
XColors.Crimson, XColor.FromArgb(0, XColors.Crimson));
gfx.DrawRectangle(brush, r);
});

Panel(2, 3, "Under a graphics transform", r =>
{
// The graphics' own transform applies to the brush as it does to the shape.
var state = gfx.Save();
gfx.TranslateTransform(Middle(r).X, Middle(r).Y);
gfx.RotateTransform(-20);
gfx.ScaleTransform(1.6, 0.8);

var brush = new XRadialGradientBrush(new XPoint(0, 0), 0, 40, XColors.LightYellow, XColors.DarkOrange);
gfx.DrawEllipse(brush, -40, -40, 80, 80);
gfx.Restore(state);
});