DotPdf: Making your own shapes

June 8, 2015 Steve Hawley

One of the main ways of creating page content in DotPdf is to use shapes.  We give you a number of “canned” shapes that are very easy to work with (circle, rectangle, path, text, etc.), but you will probably need to make your own shapes at some point.  This article is going to show you one way to do that that is very easy.

Let’s say that you need to create a letterhead that needs to have a donut in the logo.  You could just draw the donut directly, but let’s say that you’re trying to create a whole corporate presence around donuts and you need to draw a lot of donuts in a lot of places.  The way to do that is to make a shape.

What is a donut?  It’s two circles of different radii drawn at the same center with a fill color and an outline color.  Great – these feel like things to pass to the constructor and for some properties.  We’ll start by making a Donut class that is a subclass of PdfBaseShape:

using System;
using System.Drawing;
using Atalasoft.PdfDoc.Generating.Shapes;
using Atalasoft.PdfDoc.Generating;
using Atalasoft.PdfDoc.Geometry;

 
namespace DonutShape
{
    [Serializable]
    public class Donut : PdfBaseShape
    {
        public Donut(PdfPoint center, double innerRadius, double outerRadius, IPdfColor outlineColor, double linewidth, IPdfColor fillColor)
            : base(outlineColor, linewidth, fillColor)
        {
            Center = center;
            OuterRadius = outerRadius;
            InnerRadius = innerRadius;
        }

 
        public Donut(PdfPoint center, double innerRadius, double outerRadius)
            : this(center, innerRadius, outerRadius, PdfColorFactory.FromColor(Color.Black), 1.0, PdfColorFactory.FromColor(Color.Brown))
        {
        }

 

 
        protected override PdfBaseShape CloneInstance()
        {
            return new Donut(Center, InnerRadius, OuterRadius); // colors will get set by the base class
        }

 
        protected override void DrawShape(Atalasoft.PdfDoc.Generating.Rendering.PdfPageRenderer r)
        {
            throw new NotImplementedException();
        }

 
        public PdfPoint Center { get; set; }
        public double OuterRadius { get; set; }
        public double InnerRadius { get; set; }
    }
}

 

In this case, I made two constructors, one that takes the line color, line thickness and fill color and one that creates a default.  Since colors and line styles are handled by the base class, we can just pass those on and forget about them for now.  There are really only two pieces of work: be able to clone the shape and be able to draw the shape.  Cloning is easy – just call a constructor with the center and radii.  Drawing is also easy, but let’s concentrate on that code on its own:

 
protected override void DrawShape(Atalasoft.PdfDoc.Generating.Rendering.PdfPageRenderer r)
{
    PdfCircle outer = new PdfCircle(Center, OuterRadius, OutlineColor, Style.Width, FillColor);
    PdfCircle inner = new PdfCircle(Center, InnerRadius, OutlineColor, Style.Width, PdfColorFactory.FromColor(Color.White));
    outer.Render(r);
    inner.Render(r);
}

 

In this routine, I create two circles, one for the outer circle and one for the inner.  The outer I create with the Donut’s line and fill properties.  The inner I do the same except that the fill I set to white.  I can try it out with the following test code:


 
PdfGeneratedDocument doc = new PdfGeneratedDocument();
PdfGeneratedPage page = PdfDefaultPages.Letter;
doc.Pages.Add(page);

 
Donut donut = new Donut(new PdfPoint(288, 400), 18, 100);
page.DrawingList.Add(donut);
doc.Save("donut.pdf");

 

When I run the app, I get a page that looks like this:

image

Hooray – that’s just what I expected!  There’s a problem, though.  If we draw the donut over the top of something else it will cover it up entirely, which is not what we expected:


 
PdfGeneratedDocument doc = new PdfGeneratedDocument();
PdfGeneratedPage page = PdfDefaultPages.Letter;
doc.Pages.Add(page);

 
// Add a red line from the center out
PdfPath path = new PdfPath(PdfColorFactory.FromColor(Color.Red), 8);
path.MoveTo(288, 400);
path.LineTo(500, 500);
page.DrawingList.Add(path);

 
Donut donut = new Donut(new PdfPoint(288, 400), 18, 100);
page.DrawingList.Add(donut);
doc.Save("donut.pdf");

 

image

The problem is clear – the inner circle is being painted white.  This will cover up anything under it.  In fact, we depend on that to cover up the brown donut.  The solution is to change our approach.  Instead of using two PdfCircle shapes, we’re going to use one PdfPath shape to represent both circles.  A path is a collection of operations that include move, draw line, draw Bezier curve, close the path.  The really cool thing is that a path can contain any number of possibly disjoint subpaths, so two circles are easy.  To start off with, we need a routine that give a PdfPath shape will add in a circle composed of Bezier curves.  To do this, I’ll break this out into a new method.  The code presented here is based on an article by Don Lancaster.  If you want to learn a great deal about the math behind Bezier curves, this is a great place to start.


 
protected override void DrawShape(Atalasoft.PdfDoc.Generating.Rendering.PdfPageRenderer r)
{
    PdfPath path = new PdfPath(OutlineColor, Style.Width, FillColor);
    MakeCircle(path, OuterRadius, Center);
    MakeCircle(path, InnerRadius, Center);
    path.Render(r);
}

 
private static PdfPath MakeCircle(PdfPath path, double radius, PdfPoint center)
{
    double magic = 0.551784 * radius;
    path.MoveTo(new PdfPoint(-radius, 0) + center);
    path.CurveTo(new PdfPoint(-radius, magic) + center, new PdfPoint(-magic, radius) + center, new PdfPoint(0, radius) + center);
    path.CurveTo(new PdfPoint(magic, radius) + center, new PdfPoint(radius, magic) + center, new PdfPoint(radius, 0) + center);
    path.CurveTo(new PdfPoint(radius, -magic) + center, new PdfPoint(magic, -radius) + center, new PdfPoint(0, -radius) + center);
    path.CurveTo(new PdfPoint(-magic, -radius) + center, new PdfPoint(-radius, -magic) + center, new PdfPoint(-radius, 0) + center);
    return path;
}

 

image

And, tada, here is our shape.  The question is, why isn’t the donut hole filled?  The answer is that PDF has two different ways to fill paths.  The first is called the Even-Odd Rule.  The PDF renderer figures out how many lines have been crossed as it goes left to right.  If the number is odd, it fills.  If the number is even it doesn’t.  This is the default method for filling.  The other method is called the Non-Zero Winding Rule.  It is more complicate and has to do with the direction that lines are drawn as they cross the scanline being filled.  Lines that cross bottom to top add 1 to the winding number.  Lines that cross top to bottom take one away.  If the winding number is non-zero, the scanline gets filled.  If this circle had been filled with NZW, the center would be brown and cover up the red line.

As a final best practice rule for making reusable shapes: make shapes as generic as possible and put them into their own assembly away from the rest of your application.  This will make it easier to reuse the shape in other projects and will make it more easier to reload the PDF in DotPdf for further editing.

 

About the Author

Steve Hawley

Steve was with Atalasoft from 2005 until 2015. He was responsible for the architecture and development of DotImage, and one of the masterminds behind Bacon Day. Steve has over 20 years of experience with companies like Bell Communications Research, Adobe Systems, Newfire, Presto Technologies.

Follow on Twitter More Content by Steve Hawley
Previous Article
Benefits of the PNG Image Format
Benefits of the PNG Image Format

The PNG format was designed to replace the antiquated GIF format, and to some extent, the TIFF format. It u...

Next Article
Robotron and OOP

Many years back, joe holt and I pulled the ROMs from a Robotron machine that...

Try any of our Imaging SDKs free for 30 days with Full Support

Download Now