从 ASP.NET 打印地址标签
使用 PDFSharp 在 PDF 文档中创建标签页。
引言
有时,我们需要从 ASP.Net 打印信息,并且这些信息需要以高度结构化的方式在页面上进行格式化。ASP.Net 可以轻松创建可打印的 HTML 页面,但它很少考虑页面大小、文本和图像需要出现的位置,或页面的边距和填充。
当我们想打印到预切标签页(例如 Avery 标签)上时(例如,请参见此处),我们需要非常仔细地控制打印位置。最好的方法之一是创建 PDF 文档并控制该文档中的布局,当我们打印该 PDF 文档时,打印机将尊重这些尺寸。
许可和外部库
我最近写了一篇使用 iTextSharp 作为插件库的文章。我后来了解到 iTextSharp AGPL 许可证不允许我们以真正自由的方式使用它,并且如果您希望将其用于商业应用程序,则需要购买商业许可证(2,200 美元),因此,我将在本文中使用 PDFSharp 库。PDFSharp 在 MIT 许可下发布,这意味着我们可以免费无限制地使用此代码,前提是保留版权声明。对许多人来说,这是非常可以接受的。
您可以在此处找到 PDFSharp 网站,并在此处找到他们的 SoundForge 项目页面。
使用代码
本项目将使用 PDFSharp 库创建 PDF。要使用此库,您通常需要下载源代码(可在此处找到),然后将其插入您的项目或将库编译为 DLL 并使用它。对于无法将源文件编译为 DLL 的任何人,我已经为您完成了此操作,并将其附加到本文中。您可以在本页顶部找到下载链接。如果您决定下载项目源代码,DLL 已存在于其中。
PDFSharp 库采用 MIT 许可,这意味着它完全开放且免费使用,前提是您保留版权声明。此示例适用于我们为本项目生成的源代码。
如果您想下载完整的项目,请下载本文随附的 PdfSharpLabels.zip 文件。
我在本文中多次提及 Avery 标签格式 L7163;我将在整篇文章中使用此标签格式。它的具体特性对实际项目并不重要,它只是我开始这项工作时随手拿到的第一包标签。
下面我们可以看到解决方案的项目目录。这显示了我们将用于打印标签的所有文件。
第一步是定义我们的标签格式。为此,我创建了一个表示预切标签页的实体类。
LabelFormat.vb
下面您可以看到 LabelFormat.vb 的源代码。这是一个实体类,表示一张预切标签页,例如 Avery L7163。它包含我认为准确打印标签页所需的所有重要参数,如果您正在考虑将与本文相关的类和方法用于其他目的(例如打印名片),您可能需要考虑为该应用程序创建一个实体类。
''' <summary>
''' Represents the layout of a sheet of labels such as Avery L7163.
''' </summary>
''' <remarks>All dimensions in Millimeters</remarks>
Public Class LabelFormat
''' <summary>
''' Numerical Id of the label format
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property Id As Integer
''' <summary>
''' Name of the label format (e.g. Avery L7163)
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property Name As String
''' <summary>
''' Description of label format (e.g. A4 Sheet of 99.1 x 38.1mm address labels)
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property Description As String
''' <summary>
''' Width of page in millimeters
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property PageWidth As Double
''' <summary>
''' Height of page in millimeters
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property PageHeight As Double
''' <summary>
''' Margin between top of page and top of first label in millimeters
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property TopMargin As Double
''' <summary>
''' Margin between left of page and left of first label in millimeters
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property LeftMargin As Double
''' <summary>
''' Width of individual label in millimeters
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property LabelWidth As Double
''' <summary>
''' Height of individual label in millimeters
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property LabelHeight As Double
''' <summary>
''' Padding on the left of an individual label, creates space between label edge and start of content
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property LabelPaddingLeft As Double
''' <summary>
''' Padding on the Right of an individual label, creates space between label edge and end of content
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property LabelPaddingRight As Double
''' <summary>
''' Padding on the top of an individual label, creates space between label edge and start of content
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property LabelPaddingTop As Double
''' <summary>
''' Padding on the Bottom of an individual label, creates space between label edge and end of content
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property LabelPaddingBottom As Double
''' <summary>
''' Distance between top of one label and top of label below it in millimeters
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property VerticalPitch As Double
''' <summary>
''' Distance between left of one label and left of label to the right of it in millimeters
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property HorizontalPitch As Double
''' <summary>
''' Number of labels going across the page
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property ColumnCount As Integer
''' <summary>
''' Number of labels going down the page
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property RowCount As Integer
''' <summary>
''' Instantiate a new label sheet format definition
''' </summary>
''' <remarks></remarks>
Public Sub New()
End Sub
''' <summary>
''' Instantiate a new label sheet format definition
''' </summary>
''' <param name="Id">Numerical Id of the label format</param>
''' <param name="Name">Name of the label format (e.g. Avery L7163)</param>
''' <param name="Description">Description of label format (e.g. A4 Sheet of 99.1 x 38.1mm address labels)</param>
''' <param name="PageWidth">Width of page in millimeters</param>
''' <param name="PageHeight">Height of page in millimeters</param>
''' <param name="TopMargin">Margin between top of page and top of first label in millimeters</param>
''' <param name="LeftMargin">Margin between left of page and left of first label in millimeters</param>
''' <param name="LabelWidth">Width of individual label in millimeters</param>
''' <param name="LabelHeight">Height of individual label in millimeters</param>
''' <param name="VerticalPitch">Distance between top of one label and top of label below it in millimeters</param>
''' <param name="HorizontalPitch">Distance between left of one label and left of label to the right of it in millimeters</param>
''' <param name="ColumnCount">Number of labels going across the page</param>
''' <param name="RowCount">Number of labels going down the page</param>
''' <param name="LabelPaddingLeft">Padding on the left of an individual label, creates space between label edge and start of content</param>
''' <param name="LabelPaddingRight">Padding on the Right of an individual label, creates space between label edge and end of content</param>
''' <param name="LabelPaddingTop">Padding on the top of an individual label, creates space between label edge and start of content</param>
''' <param name="LabelPaddingBottom">Padding on the Bottom of an individual label, creates space between label edge and end of content</param>
''' <remarks></remarks>
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
既然我们有了一个表示标签页的实体类,我们需要一种从数据库或集合中用记录填充此类的方法。这就是 LabelFormatBLL.vb 类的目的。
通常我们会从数据库中检索记录,但为了本文的目的,我只是手动填充了记录。您可以在下面看到这一点。
''' <summary>
''' Business logic layer for label formats.
''' </summary>
''' <remarks>As this is a demo and there is no database to use, the BLL will do the DAL work aswell.</remarks>
Public Class LabelFormatBLL
Private Shared _mLabelFormats As List(Of LabelFormat)
''' <summary>
''' Return a list of all label formats in the database.
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Shared Function GetLabelFormats() As List(Of LabelFormat)
If IsNothing(_mLabelFormats) Then
' We are not using a database so manually create the labels here.
_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
''' <summary>
''' Return a single label format
''' </summary>
''' <param name="Id">integer reference for the label that is required.</param>
''' <returns></returns>
''' <remarks></remarks>
Public Shared Function GetLabelFormat(ByVal Id As Integer) As LabelFormat
GetLabelFormat = Nothing
For Each lf As LabelFormat In GetLabelFormats()
If lf.Id = Id Then
' Label format found. Return it.
Return lf
End If
Next
End Function
End Class
有两个值得关注的方法,一个是 `GetLabelFormats()`,它返回所有标签格式,第二个方法是 `GetLabelFormat()`,它根据该标签的 ID 引用返回单个格式。
我们将使用 `GetLabelFormats()` 填充一个下拉列表,允许用户选择他们想要打印的格式,`GetLabelFormat()` 函数将在选择单个格式后用于检索参数。
AddressesBLL.vb
AddressesBLL 用于为项目生成一些示例数据。它创建一个包含地址的字符串列表,以便我们可以打印地址标签。由于这仅用于示例数据,我不会详细解释其用法。
PdfLabelUtil.vb
PdfLabelUtil.vb 类是本项目的主要焦点,该类完成了所有的格式化和文档创建。我在下面展示了代码,我将进一步解释这些代码。
Imports System.IO
Imports PdfSharp
Imports PdfSharp.Drawing
Imports PdfSharp.Pdf
''' <summary>
''' Utility class for creating labels within a PDF document
''' </summary>
''' <remarks></remarks>
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
' The label sheet is basically a table and each cell is a single label
' Format related
Dim CellsPerPage As Integer = lf.RowCount * lf.ColumnCount
Dim CellsThisPage As Integer = 0
Dim ContentRectangle As XRect ' A single cell content rectangle. This is the rectangle that can be used for contents and accounts for margins and padding.
Dim ContentSize As XSize ' Size of content area inside a cell.
Dim ContentLeftPos As Double ' left edge of current content area.
Dim ContentTopPos As Double ' Top edge of current content area
' Layout related
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 ' Counts each itteration. Used with QtyEachLabel
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)
' Ensure that at least 1 of each label is printed.
If QtyEachLabel < 1 Then QtyEachLabel = 1
' Define the content area size
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
' We actually have addresses to output.
For Each Address As String In Addresses
' Once for each address
For LoopTemp = 1 To QtyEachLabel
' Once for each copy of this address.
If CellsThisPage = CellsPerPage Then
' This pages worth of cells are filled up. Create a new page
AddPage(Doc, page, lf)
Gfx = XGraphics.FromPdfPage(page)
CellsThisPage = 0
End If
' Calculate which row and column we are working on.
CurrentColumn = (CellsThisPage + 1) Mod lf.ColumnCount
CurrentRow = Fix((CellsThisPage + 1) / lf.ColumnCount)
If CurrentColumn = 0 Then
' This occurs when you are working on the last column of the row.
' This affects the count for column and row
CurrentColumn = lf.ColumnCount
Else
' We are not viewing the last column so this number will be decremented by one.
CurrentRow = CurrentRow + 1
End If
' Calculate the left position of the current cell.
ContentLeftPos = ((CurrentColumn - 1) * lf.HorizontalPitch) + lf.LeftMargin + lf.LabelPaddingLeft
' Calculate the top position of the current cell.
ContentTopPos = ((CurrentRow - 1) * lf.VerticalPitch) + lf.TopMargin + lf.LabelPaddingTop
' Define the content rectangle.
ContentRectangle = New XRect(New XPoint(XUnit.FromMillimeter(ContentLeftPos).Point, XUnit.FromMillimeter(ContentTopPos).Point),
ContentSize)
Path = New XGraphicsPath
' Add the address string to the page.
Path.AddString(Address,
New XFontFamily("Arial"),
XFontStyle.Regular,
11,
ContentRectangle,
XStringFormats.TopLeft)
Gfx.DrawPath(Pen, Brush, Path)
' Increment the cell count
CellsThisPage = CellsThisPage + 1
Next LoopTemp
Next
' Output the document
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
我们首先必须导入对 PDFSharp 库的引用。我们不需要导入 Drawing 和 Pdf 部分,但它使类中其余部分的语法更易于阅读。
Imports PdfSharp
Imports PdfSharp.Drawing
Imports PdfSharp.Pdf
我们从类外部调用的方法是 `GeneratePDFLabels()`,它返回一个表示 PDF 文档的 `MemoryStream` 对象。
一开始,我们声明所有变量,然后定义颜色和绘图对象。所有文本都作为路径在画布上绘制,`Pen` 用于绘制文本的轮廓,`Stroke` 用于填充文本。这允许我们拥有不同的轮廓和填充颜色。
然后我们定义一些用于计数迭代的变量,然后定义文档对象和页面对象。您将看到在页面定义后立即调用 `AddPage()`。这个小方法使用标签格式中定义的宽度和高度在 PDF 文档中创建一个新的 PDF 页面。
Page = Doc.AddPage
Page.Width = XUnit.FromMillimeter(lf.PageWidth)
Page.Height = XUnit.FromMillimeter(lf.PageHeight)
您应该注意到尺寸是从 XUnit 助手给出的。这是 PDFSharp 框架的一部分,允许您从您想要的任何单位(在本例中为毫米)设置尺寸,并确保 PDFSharp 正确缩放它们。
然后我们生成内容框。
' Define the content area size
ContentSize = New XSize(XUnit.FromMillimeter(lf.LabelWidth - lf.LabelPaddingLeft - lf.LabelPaddingRight).Point,
XUnit.FromMillimeter(lf.LabelHeight - lf.LabelPaddingTop - lf.LabelPaddingBottom).Point)
这是一个矩形,用于设置每个标签可以使用的可用空间。这定义为单个标签的宽度和高度减去定义的内部填充。填充有助于应对标签纸切割的差异以及(更重要的是)在打印机进纸时标签纸在打印机内部移动的情况。
接下来的几行代码计算我们当前正在处理的列和行,然后使用结果计算新标签的位置。
' Calculate the left position of the current cell.
ContentLeftPos = ((CurrentColumn - 1) * lf.HorizontalPitch) + lf.LeftMargin + lf.LabelPaddingLeft
' Calculate the top position of the current cell.
ContentTopPos = ((CurrentRow - 1) * lf.VerticalPitch) + lf.TopMargin + lf.LabelPaddingTop
值得注意的是,当我们计算页面上新标签的位置时,我们根据标签间距而不是标签宽度进行偏移。这是因为标签之间有时存在宽度无法解释的间隙。
既然我们知道内容区域有多大,并且知道它在页面上的位置,我们就可以创建定义文本范围的矩形。
' Define the content rectangle.
ContentRectangle = New XRect(New XPoint(XUnit.FromMillimeter(ContentLeftPos).Point, XUnit.FromMillimeter(ContentTopPos).Point),
ContentSize)
此单个标签的最后一步是添加从地址列表中获取的文本。我们可以在页面顶部定义字体和样式,但此处已完成。
Path = New XGraphicsPath
' Add the address string to the page.
Path.AddString(Address,
New XFontFamily("Arial"),
XFontStyle.Regular,
11,
ContentRectangle,
XStringFormats.TopLeft)
Gfx.DrawPath(Pen, Brush, Path)
最后,一旦文档完成并且所有标签都已添加到其中,我们需要将文档输出到内存流中。`False` 选项意味着内存流保持打开状态,这允许我们在将 PDF 输出到客户端的处理程序中读取它。
Doc.Save(GeneratePdfLabels, False)
LabelHandler.ashx
项目中的处理程序用于调用 `PdfLabelUtil.vb`,然后将生成的 PDF 输出到客户端。
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
我们可以看到,处理程序需要带有一个表示所需打印标签格式的查询字符串参数进行调用,例如 `LabelHandler.ashx?Id=1`
处理程序从 `AddressBLL` 检索地址列表,从 `LabelFormatBLL` 检索标签格式,然后调用 `PdfLabelUtil.GeneratePdfLabels`。完成后,流将作为文件名为“AddressLabels.pdf”的 PDF 文档输出。
Default.aspx
Default.aspx 仅用于提供一个用户界面,我们可以从中调用生成 PDF 文档的处理程序。
<div>
<asp:DropDownList ID="ddlLabelFormats" runat="server"></asp:DropDownList>
<br /><br />
<asp:Button ID="btnPrintLabels" runat="server" Text="Print Address Labels" />
</div>
有一个下拉列表显示所有可用的标签格式,还有一个按钮触发 `Response.Redirect` 到 `LabelHandler.ashx`。
下拉列表被填充,并且使用下面的两个代码块触发 `Response.Redirect`。
''' <summary>
''' Populate the label formats into the drop down list
''' </summary>
''' <remarks></remarks>
Private Sub LoadLabelFormats()
With ddlLabelFormats
.DataSource = LabelFormatBLL.GetLabelFormats
.DataValueField = "Id"
.DataTextField = "Name"
.DataBind()
End With
End Sub
''' <summary>
''' Print the label
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
''' <remarks></remarks>
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
' Label format selection from drop down list.
Response.Redirect("LabelHandler.ashx?Id=" & li.Value.ToString)
End If
End Sub
结果
当您调用处理程序或单击 Default.aspx 页面上的“打印”按钮时,将显示一个 PDF 文档,其中包含策略性地放置在页面上的文本(地址),以与标签页对齐。下面您可以看到打印到预切标签页上的效果。
我希望这对您有用,并且您能够在您的项目中找到一个应用程序。如果您有任何问题、疑问或担忧,请使用下面的评论部分。
历史
修订 1.0 2014-11-27 这是本文的第一版。