Login
 

Atalasoft Imaging SDK Development Blog

Document Imaging and Developer Commentary

Blog Home RSS Feed Old Archive Atalasoft.com

Working with DotPdf - Shapes or Shape Generators?


Posted by Steve Hawley
February 23, 2012 Comments



DotPdf comes with a number of built-in shapes and as I mentioned in a previous blog, it’s easy to define new shapes.  It’s very easy to make shapes, but sometimes we can get carried away and lose sense of what should be a shape and what should not be a shape.  This blog is going to be about guidelines for categorizing your page elements and infrastructure for making large-scale document production easier.

First, let’s talk about what makes a shape.  A shape should be:

  • Simple to represent in data (ie, you should be able to attach the [Serializable] attribute and not worry about what happens with default behaviors)
  • It should be blissfully unaware of the page on which it sits (or more precisely the drawing list in which it resides)
  • It should have absolutely no business logic

Now, let’s talk about what makes a shape generator.  A shape generator can/should be:

  • Simple or complex in data representation (it may pull its data from elsewhere)
  • Aware (or unaware) of page layout
  • Nearly all logic (business or layout)

Let’s take an example – suppose you want to make a system to generate ebooks on the fly and you want to have page automatic page numbering.  You could do that with a page number shape.  It fits the categories above – for representation, you need the page number, the location on the page (but you don’t really care where).  It’s pretty simple.  At least it seems pretty simple, until you start considering how page numbers are laid out in reality.  First, page numbers can be at the top or the bottom of the page.  Second, the page numbers can be on verso (left) or recto (right)  or center or alternating recto/verso.  Finally page numbers can be in either Roman (upper or lower) or Arabic numerals.  Phew.

This to me says that the actual page number itself is a shape, but everything around it is a generator that should be encapsulated.  So let’s make that generator.  First we’ll start with a few enumerations:


public enum VerticalPositionStyle
{
    TopRelative,
    BottomRelative
}

public enum HorizontalPositionStyle
{
    Center,
    Left,
    Right,
    Alternating
}

These enums will let us decide how to position the shape on the page.  Now let’s think about the data that we need to represent a page number generator.  We’ll need the page number, the name of the font resource, the font size, the offset from either the top or bottom of the page, the left and right margins and whether or not we are working in Roman numerals.  So for this: let’s lay out some properties:


public string FontResourceName { get; set; }
public double FontSize { get; set; }
public int Current { get; set; }
public double VerticalOffset { get; set; }
public double LeftMargin { get; set; }
public double RightMargin { get; set; }
public bool IsRomanNumerals { get; set; }
public VerticalPositionStyle VerticalPositionStyle { get; set; }
public HorizontalPositionStyle HorizontalPositionStyle { get; set; }

These are all straightforward – if these were my own code for release, I would null check FontResourceName and range check FontSize at the very least, but this is sample code, so better to be brief.

Given these, we can set up a constructor:


public PageNumberGenerator(string fontResourceName, double fontSize)
{
    if (fontResourceName == null) throw new ArgumentNullException("fontResourceName");
    if (fontSize <= 0) throw new ArgumentOutOfRangeException("fontSize");
    VerticalPositionStyle = VerticalPositionStyle.BottomRelative;
    HorizontalPositionStyle = HorizontalPositionStyle.Alternating;
    FontResourceName = fontResourceName;
    FontSize = fontSize;
    VerticalOffset = 72 * .75;
    LeftMargin = 72.0;
    RightMargin = 72.0;
    IsRomanNumerals = false;
    Current = 1;
}

Again, no surprises.  So how do we generate the content?  For this, I broke it out very procedurally:


public virtual IPdfRenderable NextPageNumber(PdfGeneratedPage page)
{
    double y = GetYPosition(page);
    double[] xs = GetXStartAndEnd(page);
    PdfTextBox textBox = new PdfTextBox(new PdfBounds(xs[0], y, Math.Abs(xs[1] - xs[0]), FontSize + 2), FontResourceName, FontSize);
    textBox.Alignment = GetAlignment();
    textBox.Text = GetNumberText();
    AdvanceCurrent();
    return textBox;
}

In this code, I get the y position of the text and the start and end x coordinates.  I make a PdfTextBox with bounds set so that the box covers the entire distance from left margin to right margin at the given y coordinate.  This isn’t strictly necessary – we could just figure out where to put the text and use a PdfTextLine, but PdfTextBox will do all the alignment for us, so why sweat it?  Finally, we set the text of the box, advance the page number and move on.

With the process written, we can worry about filling in the sub tasks.  First – getting the y position:


protected virtual double GetYPosition(PdfGeneratedPage page)
{
    switch (VerticalPositionStyle)
    {
        case VerticalPositionStyle.BottomRelative:
            return VerticalOffset;
        case VerticalPositionStyle.TopRelative:
            return page.MediaBox.Top - VerticalOffset;
        default:
            throw new ArgumentException("VerticalPositionStyle", "VerticalPositionStyle is out of range");
    }
}

For this, we calculate Y based on whether it needs to be relative to the bottom or the top.  Now – getting the x positions:


protected virtual double[] GetXStartAndEnd(PdfGeneratedPage page)
{
    double[] xs = new double[2];
    xs[0] = LeftMargin;
    xs[1] = page.MediaBox.Right - RightMargin;
    return xs;
}

Again, straight forward.  I don’t like making the array, but I like it more than using out parameters.  It’d be nice to have tuples.  Now getting the alignment:


protected virtual PdfTextAlignment GetAlignment()
{
    switch (HorizontalPositionStyle)
    {
        case HorizontalPositionStyle.Center:
            return PdfTextAlignment.Center;
        case HorizontalPositionStyle.Left:
            return PdfTextAlignment.Left;
        case HorizontalPositionStyle.Right:
            return PdfTextAlignment.Right;
        case HorizontalPositionStyle.Alternating:
            return (Current & 1) == 0 ? PdfTextAlignment.Left : PdfTextAlignment.Right;
        default:
            throw new ArgumentException("HorizontalPositionStyle", "HorizontalPositionStyle is out of range");
    }
}

How easy!  The only trick is for alternating – I use the 0 bit of the page number to determine odd/even and switch between left and right based on that.  Now getting the number text and advancing the number:


protected virtual string GetNumberText()
{
    return IsRomanNumerals ? Current.ToRoman(false) : Current.ToString();
}

protected virtual void AdvanceCurrent()
{
    Current += 1;
}

For Roman numerals, I made an extension method on int that creates a string in Roman numerals from the int in either upper or lower case.  I found a very nice implementation here and I wrapped it up in an extension method.  I’ll post the whole thing at the end.  So how does this feel when it’s put into usage?  Not bad at all:


PdfGeneratedDocument doc = new PdfGeneratedDocument();
string tnr = doc.Resources.Fonts.AddFromFontName("Times New Roman");

PageNumberGenerator gen = new PageNumberGenerator(tnr, 10);

gen.IsRomanNumerals = true;
gen.HorizontalPositionStyle = HorizontalPositionStyle.Center;
for (int i = 0; i < 10; i++)
{
    PdfGeneratedPage page = PdfDefaultPages.Letter;
    page.DrawingList.Add(gen.NextPageNumber(page));
    doc.Pages.Add(page);
}

gen.IsRomanNumerals = false;
gen.HorizontalPositionStyle = HorizontalPositionStyle.Alternating;
gen.Current = 1;
for (int i = 0; i < 5; i++)
{
    PdfGeneratedPage page = PdfDefaultPages.Letter;
    page.DrawingList.Add(gen.NextPageNumber(page));
    doc.Pages.Add(page);
}
doc.Save("numberedpages.pdf");

In this code I’m making 15 pages, the first 5 are in Roman numerals, the rest in Arabic.  Now as you’re reading through this code you’ll notice that all the procedural steps are protected virtual methods.  Why?  The answer is that by sublcassing the PageNumberGenerator you could make a number of changes that are tuned to your needs.  For example, you could override the GetNumberText() method and make it look like this:


protected override string GetNumberTest()
{
    string num = super.GetNumerText();
    return "[" + "]";
}

and put each page number in a box.  You could also do something much more flexible like this:


public string FormatString { get; set; }

protected override string GetNumberText()
{
    string text = super.GetNumberText();
    return String.Format(FomatString ?? text, text);
}

which would let you use any formatting text.  You could set it to “Page {0} of 100”.

You could also override the NextPageNumber method and call the main code but add adornments like lines around the text or embed the page number in a colored circle.

You can see that by making careful decisions about whether a page object is a shape or comes from a shape generator, it’s easy to make very flexible code.  Next time, I’ll talk about how you can generalize the shape generator concept and treat PDF generation as a process of corralling sets of page generators.

Here are the classes used in their entirety.

First PageNumberGenerator:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Atalasoft.PdfDoc.Generating.Rendering;
using Atalasoft.PdfDoc.Generating;
using Atalasoft.PdfDoc.Generating.Shapes;
using Atalasoft.PdfDoc.Geometry;

namespace PageNumberer
{
    public enum VerticalPositionStyle
    {
        TopRelative,
        BottomRelative
    }

    public enum HorizontalPositionStyle
    {
        Center,
        Left,
        Right,
        Alternating
    }

    public class PageNumberGenerator
    {
        public PageNumberGenerator(string fontResourceName, double fontSize)
        {
            if (fontResourceName == null) throw new ArgumentNullException("fontResourceName");
            if (fontSize <= 0) throw new ArgumentOutOfRangeException("fontSize");
            VerticalPositionStyle = VerticalPositionStyle.BottomRelative;
            HorizontalPositionStyle = HorizontalPositionStyle.Alternating;
            FontResourceName = fontResourceName;
            FontSize = fontSize;
            VerticalOffset = 72 * .75;
            LeftMargin = 72.0;
            RightMargin = 72.0;
            IsRomanNumerals = false;
            Current = 1;
        }

        public virtual IPdfRenderable NextPageNumber(PdfGeneratedPage page)
        {
            double y = GetYPosition(page);
            double[] xs = GetXStartAndEnd(page);
            PdfTextBox textPath = new PdfTextBox(new PdfBounds(xs[0], y, Math.Abs(xs[1] - xs[0]), FontSize + 2), FontResourceName, FontSize);
            textPath.Alignment = GetAlignment();
            textPath.Text = GetNumberText();
            AdvanceCurrent();
            return textPath;
        }

        protected virtual double GetYPosition(PdfGeneratedPage page)
        {
            switch (VerticalPositionStyle)
            {
                case VerticalPositionStyle.BottomRelative:
                    return VerticalOffset;
                case VerticalPositionStyle.TopRelative:
                    return page.MediaBox.Top - VerticalOffset;
                default:
                    throw new ArgumentException("VerticalPositionStyle", "VerticalPositionStyle is out of range");
            }
        }

        protected virtual double[] GetXStartAndEnd(PdfGeneratedPage page)
        {
            double[] xs = new double[2];
            xs[0] = LeftMargin;
            xs[1] = page.MediaBox.Right - RightMargin;
            return xs;
        }

        protected virtual PdfTextAlignment GetAlignment()
        {
            switch (HorizontalPositionStyle)
            {
                case HorizontalPositionStyle.Center:
                    return PdfTextAlignment.Center;
                case HorizontalPositionStyle.Left:
                    return PdfTextAlignment.Left;
                case HorizontalPositionStyle.Right:
                    return PdfTextAlignment.Right;
                case HorizontalPositionStyle.Alternating:
                    return (Current & 1) == 0 ? PdfTextAlignment.Left : PdfTextAlignment.Right;
                default:
                    throw new ArgumentException("HorizontalPositionStyle", "HorizontalPositionStyle is out of range");
            }
        }

        protected virtual string GetNumberText()
        {
            return IsRomanNumerals ? Current.ToRoman(false) : Current.ToString();
        }

        protected virtual void AdvanceCurrent()
        {
            Current += 1;
        }

        public string FontResourceName { get; set; }
        public double FontSize { get; set; }
        public int Current { get; set; }
        public double VerticalOffset { get; set; }
        public double LeftMargin { get; set; }
        public double RightMargin { get; set; }
        public bool IsRomanNumerals { get; set; }
        public VerticalPositionStyle VerticalPositionStyle { get; set; }
        public HorizontalPositionStyle HorizontalPositionStyle { get; set; }
    }

}

and now RomanConverter:


using System;

namespace PageNumberer
{
    public static class RomanConverter
    {
        private static int[] values = new int[] { 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1 };
        private static string[] numerals = new string[] { "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I" };

        public static string ToRoman(this int number, bool upper)
        {
            if (number < 0 || number > 3999)
                throw new ArgumentOutOfRangeException("number", "Value must be in the range 0 - 3,999.");
            if (number == 0) return "N";

            StringBuilder sb = new StringBuilder();

            for (int i = 0; i < 13; i++)
            {
                while (number >= values[i])
                {
                    number -= values[i];
                    sb.Append(numerals[i]);
                }
            }
            return upper ? sb.ToString() : sb.ToString().ToLower();
        }
    }
}

Finally the test code:


using System;
using Atalasoft.PdfDoc.Generating;

namespace PageNumberer
{
    class Program
    {
        static void Main(string[] args)
        {
            PdfGeneratedDocument doc = new PdfGeneratedDocument();
            string tnr = doc.Resources.Fonts.AddFromFontName("Times New Roman");

            PageNumberGenerator gen = new PageNumberGenerator(tnr, 10);

            gen.IsRomanNumerals = true;
            gen.HorizontalPositionStyle = HorizontalPositionStyle.Center;
            for (int i = 0; i < 10; i++)
            {
                PdfGeneratedPage page = PdfDefaultPages.Letter;
                page.DrawingList.Add(gen.NextPageNumber(page));
                doc.Pages.Add(page);
            }

            gen.IsRomanNumerals = false;
            gen.HorizontalPositionStyle = HorizontalPositionStyle.Alternating;
            gen.Current = 1;
            for (int i = 0; i < 5; i++)
            {
                PdfGeneratedPage page = PdfDefaultPages.Letter;
                page.DrawingList.Add(gen.NextPageNumber(page));
                doc.Pages.Add(page);
            }
            doc.Save("numberedpages.pdf");
        }
    }
}


Posted: 2/23/2012 10:18:52 AM by Steve Hawley | with 0 comments


Trackback URL: http://www.atalasoft.com/trackback/0ab0e257-d09a-4303-b3d9-85169e02e41d/Working-with-DotPdf-Shapes-or-Shape-Generators.aspx?culture=en-US

Comments
Blog post currently doesn't have any comments.

Subscribe

Register to receive our monthly newsletter.
preload preload preload