Introduction
Sometimes we need to print information from ASP.Net that needs to be formatted on the page in a very structured manner. ASP.Net can easily create a HTML page which can be printed, but this gives little consideration to how large the page is, where the text and images need to appear, or the margins and padding on the page.
When we want to print onto a sheet of pre-cut labels such as Avery labels (see here for example) we need to control the printing position very carefully. One of the best ways to do this is to create a PDF document and control the layout within that document, when we come to print that PDF document out these dimensions will be respected by the printer.
Licensing and External Libraries
I recently wrote an article used iTextSharp as a plug in library. I have since learnt that the iTextSharp AGPL license does not allow us to use it in a truely free manner and requires you to purchase a commercial license ($2,200) if you wish to use it in a commercial application, for this reason I will be using the PDFSharp library in this article. PDFSharp is released under the MIT license which means that we can use this code without restriction for no cost provided that we leave the copyright notices in place. For many people this is very acceptable.
You can find the PDFSharp website here, and their SoundForge project page here.
Using the code
This project will be using the PDFSharp library to create PDFs. To use this library you normally need to download the source code (found here), and then either insert the source code into your project or compile the library into a DLL and use that. For anyone who does not have the ability to compile the source files into a DLL I have done this for you and attached it to the article. You will find a download link at the top of this page. The DLL is already present in the source code for the project if you decide to download that.
The PDFSharp library is MIT licensed meaning that it is completely open and free to use provided you leave the copyright notices intact. The sample applies to the source code we have generated for this project.
If you want to download the complete project please download the file PdfSharpLabels.zip attached to this article.
I make several references to Avery Label format L7163 throughout this article; I will be using this label format throughout the article. It's specific characteristics are not important to the actual project, it was simple the first pack of labels I laid my hands on when starting this work.
Below we can see the project directory for our solution. This shows all of the files that we will be using to print labels.
The first step is to define our label format. To do this I have created an entity class that represents a sheet of pre-cut labels.
LabelFormat.vb
Below you can see the source code for LabelFormat.vb. This is an entity class that represents a single sheet of pre-cut labels such as Avery L7163. It contains all parameters that I have found important to accurately printing on a sheet of labels, if you are considering using the classes and methods associated with this article for another purpose (e.g. printing business cards) you may want to consider making an entity class for that application.
Public Class LabelFormat
Public Property Id As Integer
Public Property Name As String
Public Property Description As String
Public Property PageWidth As Double
Public Property PageHeight As Double
Public Property TopMargin As Double
Public Property LeftMargin As Double
Public Property LabelWidth As Double
Public Property LabelHeight As Double
Public Property LabelPaddingLeft As Double
Public Property LabelPaddingRight As Double
Public Property LabelPaddingTop As Double
Public Property LabelPaddingBottom As Double
Public Property VerticalPitch As Double
Public Property HorizontalPitch As Double
Public Property ColumnCount As Integer
Public Property RowCount As Integer
Public Sub New()
End Sub
Public Sub New(ByVal Id As Integer,
ByVal Name As String,
ByVal Description As String,
ByVal PageWidth As Double,
ByVal PageHeight As Double,
ByVal TopMargin As Double,
ByVal LeftMargin As Double,
ByVal LabelWidth As Double,
ByVal LabelHeight As Double,
ByVal VerticalPitch As Double,
ByVal HorizontalPitch As Double,
ByVal ColumnCount As Integer,
ByVal RowCount As Integer,
Optional ByVal LabelPaddingLeft As Double = 0.0,
Optional ByVal LabelPaddingRight As Double = 0.0,
Optional ByVal LabelPaddingTop As Double = 0.0,
Optional ByVal LabelPaddingBottom As Double = 0.0)
Me.Id = Id
Me.Name = Name
Me.Description = Description
Me.PageWidth = PageWidth
Me.PageHeight = PageHeight
Me.TopMargin = TopMargin
Me.LeftMargin = LeftMargin
Me.LabelWidth = LabelWidth
Me.LabelHeight = LabelHeight
Me.VerticalPitch = VerticalPitch
Me.HorizontalPitch = HorizontalPitch
Me.ColumnCount = ColumnCount
Me.RowCount = RowCount
Me.LabelPaddingLeft = LabelPaddingLeft
Me.LabelPaddingRight = LabelPaddingRight
Me.LabelPaddingTop = LabelPaddingTop
Me.LabelPaddingBottom = LabelPaddingBottom
End Sub
End Class
LabelFormatBLL.vb
Now that we have an entity class that represents a sheet of labels we need a way of populating this class with records from a database or collection. This is the purpose of the LabelFormatBLL.vb class.
Normally we would retrieve records from a database, but for the purpose of this article I have simply populated the records manually. You can see this below.
Public Class LabelFormatBLL
Private Shared _mLabelFormats As List(Of LabelFormat)
Public Shared Function GetLabelFormats() As List(Of LabelFormat)
If IsNothing(_mLabelFormats) Then
_mLabelFormats = New List(Of LabelFormat)
_mLabelFormats.Add(New LabelFormat(Id:=1,Name:="L7163", Description:="A4 Sheet of 99.1 x 38.1mm address labels", PageWidth:=210,PageHeight:=297,TopMargin:=15.1,LeftMargin:=4.7,LabelWidth:=99.1,LabelHeight:=38.1,VerticalPitch:=38.1,HorizontalPitch:=101.6,ColumnCount:=2,RowCount:=7,LabelPaddingTop:=5.0,LabelPaddingLeft:=8.0))
_mLabelFormats.Add(New LabelFormat(Id:=2,Name:="L7169",Description:="A4 Sheet of 99.1 x 139mm BlockOut (tm) address labels",PageWidth:=210,PageHeight:=297,TopMargin:=9.5,LeftMargin:=4.6,LabelWidth:=99.1,LabelHeight:=139,VerticalPitch:=139,HorizontalPitch:=101.6,ColumnCount:=2,RowCount:=2,LabelPaddingTop:=5.0,LabelPaddingLeft:=8.0))
End If
Return _mLabelFormats
End Function
Public Shared Function GetLabelFormat(ByVal Id As Integer) As LabelFormat
GetLabelFormat = Nothing
For Each lf As LabelFormat In GetLabelFormats()
If lf.Id = Id Then
Return lf
End If
Next
End Function
End Class
There are two methods of interest, one is GetLabelFormats()
which returns all label formats, and the second method is GetLabelFormat()
which returns a single format based on the ID reference for that label.
We would use GetLabelFormats()
to populate a dropdown that allows the use to select which format they want to print to, the function GetLabelFormat()
will be used to retrieve parameters once the individual format has been selected.
AddressesBLL.vb
AddressesBLL is used to produce some sample data for the project. It creates a list(of string) containing addresses so that we can print address labels. As this is only used for sample data I will not go into any detail explaining its use.
PdfLabelUtil.vb
The class PdfLabelUtil.vb is the main focus of this project, this class does all of the formatting and document creation. I have shown the code below, I will explain this code further on.
Imports System.IO
Imports PdfSharp
Imports PdfSharp.Drawing
Imports PdfSharp.Pdf
Public Class PdfLabelUtil
Public Shared Function GeneratePdfLabels(ByVal Addresses As List(Of String),
ByVal lf As LabelFormat,
Optional ByVal QtyEachLabel As Integer = 1) As MemoryStream
GeneratePdfLabels = New MemoryStream
Dim CellsPerPage As Integer = lf.RowCount * lf.ColumnCount
Dim CellsThisPage As Integer = 0
Dim ContentRectangle As XRect Dim ContentSize As XSize Dim ContentLeftPos As Double Dim ContentTopPos As Double
Dim StrokeColor As XColor = XColors.DarkBlue
Dim FillColor As XColor = XColors.DarkBlue
Dim Pen As XPen = New XPen(StrokeColor, 0.1)
Dim Brush As XBrush = New XSolidBrush(FillColor)
Dim Gfx As XGraphics
Dim Path As XGraphicsPath
Dim LoopTemp As Integer = 0 Dim CurrentColumn As Integer = 1
Dim CurrentRow As Integer = 1
Dim Doc As New PdfDocument
Dim page As PdfPage = Nothing
AddPage(Doc, page, lf)
Gfx = XGraphics.FromPdfPage(page)
If QtyEachLabel < 1 Then QtyEachLabel = 1
ContentSize = New XSize(XUnit.FromMillimeter(lf.LabelWidth - lf.LabelPaddingLeft - lf.LabelPaddingRight).Point,
XUnit.FromMillimeter(lf.LabelHeight - lf.LabelPaddingTop - lf.LabelPaddingBottom).Point)
If Not IsNothing(Addresses) Then
If Addresses.Count > 0 Then
For Each Address As String In Addresses
For LoopTemp = 1 To QtyEachLabel
If CellsThisPage = CellsPerPage Then
AddPage(Doc, page, lf)
Gfx = XGraphics.FromPdfPage(page)
CellsThisPage = 0
End If
CurrentColumn = (CellsThisPage + 1) Mod lf.ColumnCount
CurrentRow = Fix((CellsThisPage + 1) / lf.ColumnCount)
If CurrentColumn = 0 Then
CurrentColumn = lf.ColumnCount
Else
CurrentRow = CurrentRow + 1
End If
ContentLeftPos = ((CurrentColumn - 1) * lf.HorizontalPitch) + lf.LeftMargin + lf.LabelPaddingLeft
ContentTopPos = ((CurrentRow - 1) * lf.VerticalPitch) + lf.TopMargin + lf.LabelPaddingTop
ContentRectangle = New XRect(New XPoint(XUnit.FromMillimeter(ContentLeftPos).Point, XUnit.FromMillimeter(ContentTopPos).Point),
ContentSize)
Path = New XGraphicsPath
Path.AddString(Address,
New XFontFamily("Arial"),
XFontStyle.Regular,
11,
ContentRectangle,
XStringFormats.TopLeft)
Gfx.DrawPath(Pen, Brush, Path)
CellsThisPage = CellsThisPage + 1
Next LoopTemp
Next
Doc.Save(GeneratePdfLabels, False)
End If
End If
End Function
Private Shared Sub AddPage(ByRef Doc As PdfDocument,
ByRef Page As PdfPage,
ByVal lf As LabelFormat)
Page = Doc.AddPage
Page.Width = XUnit.FromMillimeter(lf.PageWidth)
Page.Height = XUnit.FromMillimeter(lf.PageHeight)
End Sub
End Class
We first have to import references to the PDFSharp library. We do not need to import the Drawing and Pdf sections but it makes the syntax in the rest of the class easier to read.
Imports PdfSharp
Imports PdfSharp.Drawing
Imports PdfSharp.Pdf
The method we call from outside the class is GeneratePDFLabels()
and this returns a MemoryStream
object that represents a PDF document.
At the start we declare all of our variables and go on to define our colours and drawing objects. All text is drawn on the canvas as paths, the Pen
is used for drawing the outline of the text and the Stroke
is used for filling in the text. This allows us to have different outline and fill colours.
We then define some variables that are used for counting itterations and then we define the document object and page object. You will see a call to AddPage()
immediately after the page is defined. This small method creates a new PDF page within the PDF document using the width and height that are defined in the label format.
Page = Doc.AddPage
Page.Width = XUnit.FromMillimeter(lf.PageWidth)
Page.Height = XUnit.FromMillimeter(lf.PageHeight)
You should notice that the dimensions are given from the XUnit helper. This is part of the PDFSharp framework and allows you to set dimensions from any unit you desire (in this case millimeters) and ensures that PDFSharp scales them correctly.
We then generate our content box.
ContentSize = New XSize(XUnit.FromMillimeter(lf.LabelWidth - lf.LabelPaddingLeft - lf.LabelPaddingRight).Point,
XUnit.FromMillimeter(lf.LabelHeight - lf.LabelPaddingTop - lf.LabelPaddingBottom).Point)
This is a rectangle that sets the usable space that each label can use. This is defined as the width and height of the individual label, minus the internal padding that is defined. The padding is useful to allow for variances in the cut of the label sheet and (more importantly) for the sheet of labels moving inside the printer while it is being fed.
the next few lines of code calculate which column and row we are currently working on, the results are then used to calculate the position of the new label.
ContentLeftPos = ((CurrentColumn - 1) * lf.HorizontalPitch) + lf.LeftMargin + lf.LabelPaddingLeft
ContentTopPos = ((CurrentRow - 1) * lf.VerticalPitch) + lf.TopMargin + lf.LabelPaddingTop
It is interesting to note that when we calculate the position of the new label on the page we offset according to the label pitch, not the label width. This is because there are sometimes gaps between the labels which the width would not account for.
Now that we know how large the content area is and we know where on the page it is we can create the rectangle that defines the confines of the text.
ContentRectangle = New XRect(New XPoint(XUnit.FromMillimeter(ContentLeftPos).Point, XUnit.FromMillimeter(ContentTopPos).Point),
ContentSize)
The last step for this individual label is to add the text which is taken from the address list. We could have defined the font and style at the top of the page but instead it was done here.
Path = New XGraphicsPath
Path.AddString(Address,
New XFontFamily("Arial"),
XFontStyle.Regular,
11,
ContentRectangle,
XStringFormats.TopLeft)
Gfx.DrawPath(Pen, Brush, Path)
Finally, once the document has been completed and all labels have been added to it we need to output the document to the memory stream. The False
option means that the memorystream is left open, this allows us to read it in the handler that will output the PDF to the client.
Doc.Save(GeneratePdfLabels, False)
LabelHandler.ashx
The handler in the project is used to call PdfLabelUtil.vb
and then output the resulting PDF to the client.
Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
If IsNumeric(context.Request.QueryString("Id")) Then
Using MemStream As MemoryStream = PdfLabelUtil.GeneratePdfLabels(AddressesBLL.GetAddresses,
LabelFormatBLL.GetLabelFormat(CInt(context.Request.QueryString("Id"))),
1)
If Not IsNothing(MemStream) Then
With context.Response
.Clear()
.ContentType = "application/pdf"
.AddHeader("content-length", MemStream.Length.ToString())
.AppendHeader("Content-Disposition", "inline; filename=AddressLabels.pdf")
.BinaryWrite(MemStream.ToArray())
.Flush()
.Close()
.End()
End With
End If
End Using
Else
context.Response.ContentType = "text/plain"
context.Response.Write("ID Missing")
End If
End Sub
We can see that the handler needs to be called with a querystring parameter representing the label format that is desired for printing out, e.g. LabelHandler.ashx?Id=1
The handler retrieves the list of addresses from AddressBLL
, and the label format from LabelFormatBLL
before calling PdfLabelUtil.GeneratePdfLabels
. Once complete the stream is output as a PDF document with the filename "AddressLabels.pdf".
Default.aspx
Default.aspx is used simply to provide a user interface from which we can call the handler which generates the PDF document.
<div>
<asp:DropDownList ID="ddlLabelFormats" runat="server"></asp:DropDownList>
<br /><br />
<asp:Button ID="btnPrintLabels" runat="server" Text="Print Address Labels" />
</div>
There is a dropdown which displays all of the available label formats and a button that triggers the Response.Redirect
to LabelHandler.ashx
.
The dropdown is populated, and the Response.Redirect
is triggered using the two code blocks below.
Private Sub LoadLabelFormats()
With ddlLabelFormats
.DataSource = LabelFormatBLL.GetLabelFormats
.DataValueField = "Id"
.DataTextField = "Name"
.DataBind()
End With
End Sub
Private Sub btnPrintLabels_Click(sender As Object, e As EventArgs) Handles btnPrintLabels.Click
Dim li As ListItem = CType(ddlLabelFormats.SelectedItem, ListItem)
If Not IsNothing(li) Then
Response.Redirect("LabelHandler.ashx?Id=" & li.Value.ToString)
End If
End Sub
Results
When you call the handler or click the 'Print' button on the Default.aspx page you will be presented with a PDF document of text (addresses) strategically placed on the page to line up with a sheet of labels. Below you can see what this looks like when you print out onto the pre-cut label sheet.
I hope this is useful to you and you manage to find an application within your projects. If you have any problems, questions or concerns please use the comments section below.
History
Revision 1.0 2014-11-27 This is the first copy of this article.