What is Behind the Rendering Design of DotPdf?

November 24, 2014 Steve Hawley

In implementing DotPdf’s mechanism for rendering, I wanted a design that would map reasonably well onto the PDF rendering set, something that would be directly usable by my customers (if they chose to do so), a design that allows easy the building of a convenience layer of common things to represent in PDF, and finally a way that would make it easy for me to create a visual renderer in the future.

In this design the rendering model is represented by two basic elements: a renderable object and a renderer which responds to commands from the renderable object. A renderable object is an object that implements the interface IPdfRenderable, which amounts to this:

public interface IPdfRenderable {
    string Name { get; set; }
    void Render(PdfPageRenderer r);
}

This is small and not particularly onerous – we like that. Technically, the Name property is not necessary, however, I’ve found that it’s often useful to have a tag associated with each and every element in any system. I don’t dictate how it will be used—that’s up to you.

A PdfPageRenderer object is a class that implements the rendering model for PDF pages. Since PDF makes a distinction between rendering text  and graphics (for no particularly good reason other than resource management, I presume), I model this as well by giving you two separate “surfaces” one for text and one for drawing. Both surfaces require that you perform operations only after calling their Begin method and you may not call a Begin method before a previous Begin has been matched with an End. This is somewhat inconvenient, but the contract keeps the operations from generating bad PDF.

One way to test an API for usability is to write a chunk of code against it with different goals in mind. In DotPdf, I give you’re a classic OOP hierarchy of shapes with an abstract base class that defines general properties of shapes and a mechanism for rendering them without dictating the contents. As an example, I’m going to present a way of building something similar, but by instead using functional programming with F#.

To start, let’s model the operations that make up a Bezier path:

type PathOp =
    | Close
    | MoveTo of PdfPoint
    | LineTo of PdfPoint
    | CurveTo of PdfPoint * PdfPoint * PdfPoint

Which is a discriminated union that defines the elements of a path. In DotPdf, I use a single class to represent this, PdfPathOperation. Since we need to interoperate with DotPdf, here’s function to convert a PathOp to a PdfPathOperation: 

let toPdfPathOperation p =
    match p with
    | Close -> PdfPathOperation.Close()
    | MoveTo(pt) -> PdfPathOperation.MoveTo(pt)
    | LineTo(pt) -> PdfPathOperation.LineTo(pt)
    | CurveTo(cp1, cp2, endpt) -> PdfPathOperation.CurveTo(cp1, cp2, endpt)


Now we’ll put together a very simple set of elements that we would like to render: paths, rectangles, circles, and aggregations of those. In PDF, there is a definition for paths and rectangles, but not the other things. That doesn’t mean we shouldn’t model them in F#, but it does mean we’ll need a little extra support. Here is the basic model, again a discriminated union:

type RenderElement =
    | Path of PathOp list
    | Rect of PdfBounds
    | Circle of PdfPoint * float
    | ElemList of RenderElement list

This is all straight forward except that a Circle is a tuple of PdfPoint and float (center and radius) and I give you aggegration by adding a structurally recursive ElemList. Since we don’t have a way of drawing circles, we’ll put one together by simulating one with Bezier curves. Don Lancaster has a great explanation of how this works.

let circlePath (center:PdfPoint) radius =
    let magic = 0.551784 * radius
    [
        MoveTo  (center + PdfPoint(-radius, 0.0)) ;
        CurveTo((center + PdfPoint(-radius, magic),
                 center + PdfPoint(-magic, radius),
                 center + PdfPoint(0.0, radius)));
        CurveTo((center + PdfPoint(magic, radius),
                 center + PdfPoint(radius, magic),
                 center + PdfPoint(radius, 0.0)));
        CurveTo((center + PdfPoint(radius, -magic),
                 center + PdfPoint(magic, -radius),
                 center + PdfPoint(0.0, -radius)));
        CurveTo((center + PdfPoint(-magic, -radius),
                 center + PdfPoint(-radius, -magic),
                 center + PdfPoint(-radius, 0.0))) ;
        Close
    ]

This generates a list of PathOp that represents the circle. Now given a RenderElement, we need a way to instruct a renderer to draw it, so we’ll use the following function:

 
let rec add (r:PdfPageRenderer) re =
    match re with
    | Path(pol) -> r.DrawingSurface.AddPath(pol |> List.map(toPdfPathOperation))
    | Rect(bounds) -> r.DrawingSurface.AddRect(bounds)
    | Circle(center,radius) -> r.DrawingSurface.AddPath((circlePath center radius) |> List.map(toPdfPathOperation))
    | ElemList(elems) -> elems |> List.iter(add r)    

Given any RenderElement, we either add a path to the drawing surface or add a Rect. In the case of an ElemList, we recursively add its elements.

This is all well and good. It models the geometry of a bunch of different things. What we’re missing now is a way to model the appearance of the geometry. In PDF, a sequence of path operations can be filled, have the outline drawn, have the operations filled then outlined, or the geometry can be used to clip. We’ll model this with a record in F#. In reality, the style of a line is far more complicated, but here we’ll just model it as having a width. Note that we could also represent this as another discriminated union (FilledElem, OutlineElem, FilledOurlineElem, ClipElem), but in spite of the usage of null objects, this isn’t a bad model:

type SharpElem =
    {
        elem : RenderElement
        fillMethod : PdfFillMethod
        fill : IPdfColor
        stroke : IPdfColor
        width : float
        clip : bool
    }

Let’s define two functions that operate on this. This first will determine if the render element will mark the page and the second will use a renderer to render the element:

let marksPage se = se.fill <> null || se.stroke <> null
 
let render(r:PdfPageRenderer) se =
    if se.clip || marksPage se then
        r.DrawingSurface.Begin()
        add r se.elem
        if se.clip then
            r.DrawingSurface.Clip(se.fillMethod, false)
        else
            if se.fill <> null && se.stroke <> null then
                r.DrawingSurface.FillAndStroke(se.fillMethod, se.fill, PdfLineStyle(se.width), se.stroke)
            else if se.fill <> null then
                r.DrawingSurface.Fill(se.fillMethod, se.fill)
            else
                r.DrawingSurface.Stroke(PdfLineStyle(se.width), se.stroke)
        r.DrawingSurface.End()

Render is doing the heavy lifting of adding the path elements to the drawing surface then figuring out how to draw them. marksPage is used as an optimization – if we have no color for outline or fill, we shouldn’t even bother touching the drawing surface as this would make no change to the page.

We’re almost there. While we can communicate with a PdfPageRenderer, we don’t have anything that can act like an IPdfRenderable. A PdfGeneratedPage object contains a property called DrawingList which is an IList<IPdfRenderable>. In order to have our objects present on the page, we need to implement this interface. I made two. The first, SharpElemShape is a basic container for a SharpElem and the other is one that can contain an aggregation of SharpElemShape which wraps them in calls that save and restore the current graphics state. This allows us to undo clipping done by a shape.

type SharpElemShape(elem:SharpElem, name:string) =
    let mutable _name = name
    interface IPdfRenderable with
        member this.Name with get() = _name
                         and set(value:string) = _name <- value
        member this.Render(r:PdfPageRenderer) = render r elem
    member this.Name
        with get() = (this :> IPdfRenderable).Name
        and set(value:string) = (this :> IPdfRenderable).Name <- value
    member this.Render(r:PdfPageRenderer) = (this :> IPdfRenderable).Render(r)
 
type SavedShapeSet(shapes:SharpElemShape list, name:string) =
    let mutable _name = name
    interface IPdfRenderable with
        member this.Name with get() = _name
                         and set(value:string) = _name <- value
        member this.Render(r:PdfPageRenderer) =
            r.GSave()
            shapes |> List.iter(fun item -> item.Render(r))
            r.GRestore()
    member this.Name
        with get() = (this :> IPdfRenderable).Name
        and set(value:string) = (this :> IPdfRenderable).Name <- value
    member this.Render(r:PdfPageRenderer) = (this :> IPdfRenderable).Render(r)

One thing I don’t like is that each of these has a great deal of repetition. Since the only difference between the two objects are the first parameter in the constructor and the implementation of IPdfRenderable.Render, we could instead have one class that in the constructor takes two parameters: the name object and a function to do the rendering. The type of that function would be PdfPageRenderer->unit and we could use partial function application to make bind either a SharpElem or a SharpElemShape list and perform the appropriate rendering. Still, this is meant to be a simple example and even though partial function application is a handy tool, it detracts from readability.

To test out this code, I used the following snippet:

    let doc = PdfGeneratedDocument()
    let page = doc.AddPage(PdfDefaultPages.Letter)
 
    let path = [ MoveTo(PdfPoint(72.0, 72.0)) ; LineTo(PdfPoint(288.0, 144.0)) ; LineTo(PdfPoint(200.0, 144.0))
                 Close ]
    let elem = {
        elem = Path(path);
        fillMethod = PdfFillMethod.EvenOdd;
        fill = PdfColorFactory.FromRgb(1.0, 0.0, 0.0)
        stroke = null
        width = 0.0
        clip = false
        }
    let shape = SharpElemShape(elem, "triangle")
    page.DrawingList.Add(shape)
 
    let elem1 = {
        elem = Rect(PdfBounds(100.0, 110.0, 60.0, 60.0))
        fillMethod = PdfFillMethod.EvenOdd;
        fill = null
        stroke = null
        width = 1.0
        clip = true
    }
    let elem2 = {
        elem = Circle(PdfPoint(100.0, 110.0), 48.0)
        fillMethod = PdfFillMethod.EvenOdd;
        fill = PdfColorFactory.FromRgb(0.0, 0.0, 1.0)
        stroke = PdfColorFactory.FromRgb(0.0, 0.0, 0.0)
        width = 4.0
        clip = false
    }
    page.DrawingList.Add(SavedShapeSet([SharpElemShape(elem1, "clip"); SharpElemShape(elem2, "circle")], "set"))
 
    doc.Save("sharpshape.pdf")

This will create a PDF document that when viewed in DotImage or Acrobat, will look something like this:

image

You can see that the circle that we drew has been clipped to the rectangle and placed over the top of the triangle.

From this example, we see that the layering model of rendering in DotPdf is flexible enough to be tied in many different possible implementations of renderable objects. In fact, DotPdf is totally agnostic to the representation of geometry. The geometry can be done in whatever way is convenient for you and you can make the choice to keep the geometry tightly coupled with the IPdfRenderable interface (as is it in DotPdf’s PdfBaseShape class hierarchy) or completely decoupled as it is in this example.

DotPdf is an API that represents a good balance of abstraction and concrete implementation that allows the customer to choose the level of integration. Either way, the models are easy to understand and easy to use. You can download an evaluation of DotPdf and start making documents that meet your unique needs today.


 

About the Author

Steve Hawley

Steve has been with Atalasoft since 2005. Not only is he responsible for the architecture and development of DotImage, he is 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
An Introduction Is In Order…
An Introduction Is In Order…

Hello – My name is Brendan Day and I am the Sales and Marketing Director...

Next Article
Interruptions, Memory, and Health
Interruptions, Memory, and Health

Recently, I recalled attending a get-together with my friend Dan, who is...

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

Download Now