ZPLTemplate Class

This class I use along with my previous post to extract the different place holders in the template file and produce the finished output.  The full project is located here: LabelPrinter

Usage:

Dim labelTemplate as New ZPLTemplate(filePath)
labelTemplate.FieldList(“[Part Number]”) = “ABC123”
Printer.RawHelper.SendStringToPrinter(“GK420t”, labelTemplate.Output)


‘ Written by: Ray Held

Option Explicit On
Option Strict On
Option Infer Off

Imports System.IO
Imports System.Text
Imports System.Text.RegularExpressions

Public Class ZPLTemplate

Private Template As String
Private Const RegExPattern As String = “\[(.*?)\]” ‘Looks for values that are between brackets: [ExampleField]

”’
”’ This property will have all of the place holders in the file. Simply refer to the place holder by name and set the value before calling output
”’

”’
”’
”’
Public Property FieldList As New Dictionary(Of String, String)

”’
”’ Produces the output file with the chacter replacements
”’

”’
”’
”’
Public ReadOnly Property Output As String
Get
Dim out As String = Template
For Each k As String In FieldList.Keys
out = out.Replace(k, FieldList.Item(k))
Next
Return out
End Get
End Property

Private Sub New()
‘Intentionally left blank — i need an input template to make this work.
End Sub

”’
”’ Load this class with the template file you are working with
”’

”””
Public Sub New(inputTemplate As String)
Template = inputTemplate
FindFields()
End Sub

Private Sub FindFields()
Dim re As New Regex(RegExPattern)
FieldList = New Dictionary(Of String, String)

For Each m As Match In re.Matches(Template)
If Not FieldList.ContainsKey(m.ToString) Then
FieldList.Add(m.ToString, String.Empty)
End If
Next
End Sub

End Class

Get my full demo app here: LabelPrinter


Printing using the Zebra Print Language ZPL from .Net

I’ve seen a lot of people setup a report in SSRS to print a barcode label to a Zebra printer.  Not only is this a very inefficient way of printing barcode labels to a zebra printer but you also normally need to purchase a barcode font package for this purpose as well.  Why is this not the best way to print a barcode label?  Well, because when you print from SSRS (whether from code or from the report itself) an image is generated of the print job and then sent to the printer as an image.  If you just want to print a label here and there and don’t mind this slow technique, then have at it.  But, if you are printing many labels and need the high speed printing that those label printers can provide, then you’re going to have to use ZPL.  I’m not going to give you a class on writing ZPL but I will show you how you can generate a simple ZPL command file using the free ZebaDesigner label package from Zebra and then set that label up as a template that you can then use from code and get high performance printing from that template.

Obviously, your first step is to generate the template file.  You can do this manually by writing zpl into a text document or you can use the zebra designer software.  The zpl manual and software can be found on Zebra’s website, but you have to do a little bit of digging to get it.  Go to printers, select a printer from the list (I use a GK420t printer so that is what I picked — it really doesn’t matter), then at the bottom of the printers page, click on the tab for Manuals and there you will find the zpl programmers guide (ZPL II is what I use).  If you’re printer is not a ZPL printer (some are EPL, for example) you will need to find the guide for that.  The technique to print will be the same for either language.

Zebra Designer will come on your install CD for your printer.  The free version will allow you to design and print labels but will not connect to a database or excel file, etc.  but that’s ok, we’re programmers and we can make that happen in our code.  We just need a nice label layout.

First step:  Design a label.  Use the component on the tool bar to add a text box.  In the properties of the text box, instead of putting the actual value you want lets put in some sort of place holder instead so that we can find it in code later on.  I usually use brackets around my field names, like:  [Part Number]

Important:  Do not switch to a windows font.  You must leave the font as a zebra font otherwise, you will get an image generated of the text and then we won’t be able to replace the place holder later on.  By default, when you insert a text object, the font will already be set to ZEBRA 0 10.0 Pt — this is what you want.  If you click the select button, you will no longer have that option and will have to destroy the object and recreate it.  We cannot do a character replacement on an image so we need the zebra font selected as that is native to the printer.

We will do the same thing on the barcode.  Set other properties as needed.  The barcode will most likely be red because it doesn’t fit the page (at least, not on my 3 inch label I’m using).  You can resize until it fits, however, the barcode will only size and snap to proper proportions that are scanable, so keep this in mind.

2nd Step:  To get our ZPL code from the label, go to File \ Print  then check the box that say’s “Print to file.”  Then click the print button.  You will be prompted for a location to save the .prn file.  Now, open that file up in your favorite text editor and take a look at it:

CT~~CD,~CC^~CT~
^XA~TA000~JSN^LT0^MNW^MTT^PON^PMN^LH0,0^JMA^PR5,5~SD15^JUS^LRN^CI0^XZ
^XA
^MMT
^PW609
^LL0203
^LS0
^FT554,139^A0I,28,28^FH\^FD[Part Number]^FS
^BY2,3,72^FT553,53^BCI,,Y,N
^FD>:[Part Number]^FS
^PQ1,0,1,Y^XZ

The very first line of the label sets some preliminary command type information for the printer.  Unless you are changing your command information on your printer (which most users do not) I would generally delete that first line.

So, in ZPL, a label begins with a ^XA and ends with a ^XZ  If you look closely, there are actually what looks like 2 labels in this file.  I will tell you that the first label, since this came from Zebra Designer is actually just setting up the printer — label size, orientation, etc.  If you are printing a lot of labels at a time from your finished application, this first job will cause your printer to pause in between each actual label.  I generally will pull this line out and set it aside for sending to the printer prior to sending my actual batch of labels, so I only have to do it one time.  You too will be happy you did this if you’re printing a run of thousands.

All of the printer commands in this file you can look up in your ZPL II manual.  They all begin with the caret ” ^ ” followed by the 2 letter code, then the parameters (comma separated) for that code.  Looking at our template, you can see our place holders that we setup when designing the label — [Part Number].  If you were to print this label (which you can’t do at the moment but you can go back to zebra designer and print the label that way) you will see that our place holder will print out just as it is.  The thing you will see is that they are wrapped in a ^FD and ^FS — this is the field value commands.  The ^BC command is our code 128 barcode command and the ^FD that follows that is the value for the barcode.  The barcode value has a >: character just after the ^FD and before our place holder — that is dealing with special compression stuff — I’ll let you figure that one out on your on.  Unless you have a space problem on your label, I delete those compression commands because there are very specific rules and instances when you can use them anyway.  Your app will have to be smart enough to know when to inject compression commands and that is too much for this post at this time.  The ^A0  command is our text object and the ^BC is our barcode.

The ^FT commands are the placement of the objects on the label x,y coordinates in pixels.  The ^PQ command is the print quantity (I will sometimes use a place holder for that as well to give me the option of saying how many labels to print (example:  ^PQ[Print Quantity],0,1,Y  )  and all you really need anyway is ^PQ1   the rest of that fluff you can refer to your manual for.

^PW is the page width in pixels per inch (if you have a 203 dpi printer — which is common, and a 3 inch label, then this will have a ^PW609.

^LL is the label length, again in pixels.  Our ^LL0203 is for 1 inch (203 dpi printer again and we have a 3 x 1 label).

^FH is the field Hex Indicator — so you can enter hex values — I remove this unless I need it for something but it is indicating that the \ character will designate that the next pair of characters is a hex value.  We don’t need this.

The ^BY command is setting the barcode parameters.  It will set this on the printer and will remain that way for all labels until another ^BY command is received.  I like to move that line up to the top since it is more of a configure thing but necessary for the initial label anyway.

Here is my cleaned up ZPL template (I do like to move my objects on the same line with the ^FT commands if they aren’t so that it is easier to see the whole field and I always move my ^XZ to the last line:

^XA
^MMT
^PW609
^LL0203
^LS0
^BY2,3,72
^FT554,139^A0I,28,28^FD[Part Number]^FS
^FT553,53^BCI,,Y,N^FD[Part Number]^FS
^PQ1
^XZ

That is our finished ZPL template.  You can have as many labels as you like in the same file however, please keep in mind the file size and the amount of memory available for storage in the printer.  If all of your print batch is in the same file, it may take a bit for the job to load over the network to the printer but you will get a really fast print job doing it this way, where sending as individual files will occasionally cause the printer to pause a moment as more data is received, etc.  An even faster way is to save a base template via ZPL on the printer itself, then recall that template from our print job providing the data bits we need, but you can refer to your ZPL manual for that as this isn’t really a ZPL class here.  Save that and lets work on our code.

First, you load and then do string replacements on the place holders.  We will use the example of part number AC4500XL

We need to replace the place holders with that value so that our zpl template now looks like this (I’ve highlighted the change):

^XA
^MMT
^PW609
^LL0203
^LS0
^BY2,3,72
^FT554,139^A0I,28,28^FDAC4500XL^FS
^FT553,53^BCI,,Y,N^FDAC4500XL^FS
^PQ1
^XZ

In order to get this command file to print a label on your printer, the file must be sent to the printer in RAW format.  That means, you can’t just do a simple print.  Fortunately, it is super easy to do just that.  You can just use the template file itself in your application or save it as binary in your database.   I normally will load the template into memory, do my character replacement then use the SendStringToPrinter method to print.  You can also save to a new file (useful for systems that will need another process to perform the print) then use the SendFileToPrinter method.

Here is my Printer.vb class I use for this task (sorry, it copies and pastes horribly):


 

 

Option Explicit On
Option Strict On
Option Infer Off

Imports System.IO
Imports System.Drawing.Printing
Imports System.Runtime.InteropServices

Namespace Printer

Public Class RawHelper

#Region “DLLImports”

<DllImport(“winspool.Drv”, EntryPoint:=”OpenPrinterW”, SetLastError:=True, CharSet:=CharSet.Unicode, ExactSpelling:=False, CallingConvention:=CallingConvention.StdCall)> _
Private Shared Function OpenPrinter(ByVal printerName As String, ByRef printerHandle As IntPtr, ByVal printerDefault As Integer) As Long
End Function

<DllImport(“winspool.Drv”, EntryPoint:=”ClosePrinter”, SetLastError:=True, CharSet:=CharSet.Unicode, ExactSpelling:=True, CallingConvention:=CallingConvention.StdCall)> _
Private Shared Function ClosePrinter(ByVal hPrinter As IntPtr) As Boolean
End Function

<DllImport(“winspool.Drv”, EntryPoint:=”StartDocPrinterW”, SetLastError:=True, CharSet:=CharSet.Unicode, ExactSpelling:=False, CallingConvention:=CallingConvention.StdCall)> _
Private Shared Function StartDocPrinter(ByVal hPrinter As IntPtr, ByVal level As Int32, ByRef pDI As DOCINFOW) As Boolean
End Function

<DllImport(“winspool.Drv”, EntryPoint:=”EndDocPrinter”, SetLastError:=True, CharSet:=CharSet.Unicode, ExactSpelling:=True, CallingConvention:=CallingConvention.StdCall)> _
Private Shared Function EndDocPrinter(ByVal hPrinter As IntPtr) As Boolean
End Function

<DllImport(“winspool.Drv”, EntryPoint:=”StartPagePrinter”, SetLastError:=True, CharSet:=CharSet.Unicode, ExactSpelling:=True, CallingConvention:=CallingConvention.StdCall)> _
Private Shared Function StartPagePrinter(ByVal hPrinter As IntPtr) As Boolean
End Function

<DllImport(“winspool.Drv”, EntryPoint:=”EndPagePrinter”, SetLastError:=True, CharSet:=CharSet.Unicode, ExactSpelling:=True, CallingConvention:=CallingConvention.StdCall)> _
Private Shared Function EndPagePrinter(ByVal hPrinter As IntPtr) As Boolean
End Function

<DllImport(“winspool.Drv”, EntryPoint:=”WritePrinter”, SetLastError:=True, CharSet:=CharSet.Unicode, ExactSpelling:=True, CallingConvention:=CallingConvention.StdCall)> _
Private Shared Function WritePrinter(ByVal hPrinter As IntPtr, ByVal pBytes As IntPtr, ByVal dwCount As Int32, ByRef dwWritten As Int32) As Boolean
End Function

#End Region
Private Shared Function SendBytesToPrinter(ByVal printerName As String, ByVal bytesPointer As IntPtr, ByVal bufferSize As Int32) As Boolean
Dim printerHandle As IntPtr             ‘ The printer handle
Dim printerError As Int32               ‘ Last Error
Dim di As New DOCINFOW                  ‘ Describes your document (name, port, data type)
Dim bytesWritten As Int32               ‘ The number of bytes written by WritePrinter()
Dim success As Boolean = False          ‘ success if true

‘ Set up the DOCINFO structure
di.DocumentName = System.Reflection.Assembly.GetCallingAssembly.GetName.Name
di.PrinterDataType = “RAW”

If OpenPrinter(printerName, printerHandle, 0) > 0 Then
If StartDocPrinter(printerHandle, 1, di) Then
If StartPagePrinter(printerHandle) Then
‘ Write your printer-specific bytes to the printer.
success = WritePrinter(printerHandle, bytesPointer, bufferSize, bytesWritten)
EndPagePrinter(printerHandle)
End If
EndDocPrinter(printerHandle)
End If
ClosePrinter(printerHandle)
End If

If Not success Then
printerError = Marshal.GetLastWin32Error()
End If

Return success
End Function

”’ <summary>
”’ Sends a file to the printer in RAW format
”’ </summary>
”’ <param name=”printerName”>The windows driver name/path for the printer</param>
”’ <param name=”filePath”>The file to be printed</param>
”’ <returns></returns>
”’ <remarks>Only use ANSI data as this will print in RAW format</remarks>
Public Shared Function SendFileToPrinter(ByVal printerName As String, ByVal filePath As String) As Boolean
Try
‘ Open the file.
Dim fs As New FileStream(filePath, FileMode.Open)
Dim br As New BinaryReader(fs)
Dim bufferSize As Int32 = 0

Try
bufferSize = Convert.ToInt32(fs.Length)
Catch ex As Exception
Throw New System.Exception(“The file size is too big to be processed by this engine”)
End Try

‘ Dim an array of bytes large enough to hold the file’s contents.
Dim fileData(bufferSize) As Byte
Dim success As Boolean = False

‘ Your unmanaged pointer.
Dim memoryPointer As IntPtr

‘ Read the contents of the file into the array.
fileData = br.ReadBytes(bufferSize)

‘ Allocate some unmanaged memory for those bytes.
memoryPointer = Marshal.AllocCoTaskMem(bufferSize)

‘ Copy the managed byte array into the unmanaged array.
Marshal.Copy(fileData, 0, memoryPointer, bufferSize)

‘ Send the unmanaged bytes to the printer.
success = SendBytesToPrinter(printerName, memoryPointer, bufferSize)

‘ Free the unmanaged memory that you allocated earlier.
Marshal.FreeCoTaskMem(memoryPointer)

Return success
Catch ex As Exception
Throw
End Try

End Function

”’ <summary>
”’ Send a string to the selected printer in RAW format
”’ </summary>
”’ <param name=”printerName”>The windows driver name/path for the printer</param>
”’ <param name=”stringToPrint”>The ANSI text that is to be printed to the printer</param>
”’ <returns></returns>
”’ <remarks>Only use ANSI data as this will print in RAW format</remarks>
Public Shared Function SendStringToPrinter(ByVal printerName As String, ByVal stringToPrint As String) As Boolean
Try
Dim memoryPointer As IntPtr
Dim bufferSize As Int32
Dim success As Boolean = False

bufferSize = stringToPrint.Length()
‘ Assume that the printer is expecting ANSI text, and then convert
‘ the string to ANSI text.
memoryPointer = Marshal.StringToCoTaskMemAnsi(stringToPrint)

‘ Send the converted ANSI string to the printer.
success = SendBytesToPrinter(printerName, memoryPointer, bufferSize)
Marshal.FreeCoTaskMem(memoryPointer)

Return success
Catch ex As Exception
Throw
End Try
End Function

End Class

<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)>
Friend Structure DOCINFOW
<MarshalAs(UnmanagedType.LPWStr)> Public DocumentName As String
<MarshalAs(UnmanagedType.LPWStr)> Public OutputFile As String
<MarshalAs(UnmanagedType.LPWStr)> Public PrinterDataType As String
End Structure
End Namespace