Recently I had to integrate Code 39 and Code 128 barcodes into Reporting Services. Commercial solutions range from $500 to $1000 so I looked for free fonts that could do the work for me. It seemed simple but I came across a bunch of problems and thought it might help to explain the troubles I went through. By the end of the post you’ll have a complete and free solution that you can cut and paste and know worked and didn’t work.
You can download the completed demo project (for SSRS 2008) here: Reporting Services Free Barcodes Demo.
In The Beginning…
I originally came across John T Barton’s site where he provides Code 39 and Code 128 fonts along with C# code to format input strings into something the barcode fonts can display. The source code is in here under BarcodeDLLBarcodeConverter.cs.
However he himself downloaded the fonts from the french site that created it, Grand Zebu, and they have since released newer versions of the font, which require some minor changes to the code. The reference links are here (Code 39) and here (Code 128) respectively.
I ran Barton’s code through a free online C# to VB.NET converter at developer Fusion, and modified it as best I could understand Grand Zebu’s notes above, it came out with the hideously ugly but functional code below. Disclaimer: I didn’t try to tidy it up…:
Public Shared Function StringToBarcode128String(ByVal value As String) As String ' Parameters : a string ' Return : a string which give the bar code when it is dispayed with CODE128.TTF font ' : an empty string if the supplied parameter is no good Dim charPos As Integer, minCharPos As Integer Dim currentChar As Integer, checksum As Integer Dim isTableB As Boolean = True, isValid As Boolean = True Dim returnValue As String = String.Empty If value.Length > 0 Then ' Check for valid characters For charCount As Integer = 0 To value.Length - 1 'currentChar = char.GetNumericValue(value, charPos); currentChar = AscW(Char.Parse(value.Substring(charCount, 1))) If Not (currentChar >= 32 AndAlso currentChar <= 126) Then isValid = False Exit For End If Next ' Barcode is full of ascii characters, we can now process it If isValid Then charPos = 0 While charPos < value.Length If isTableB Then ' See if interesting to switch to table C ' yes for 4 digits at start or end, else if 6 digits If charPos = 0 OrElse charPos + 4 = value.Length Then minCharPos = 4 Else minCharPos = 6 End If minCharPos = IsNumber(value, charPos, minCharPos) If minCharPos < 0 Then ' Choice table C If charPos = 0 Then ' Starting with table C ' char.ConvertFromUtf32(210); returnValue = (ChrW(210)).ToString() Else ' Switch to table C returnValue = returnValue & (ChrW(204)).ToString() End If isTableB = False Else If charPos = 0 Then ' Starting with table B ' char.ConvertFromUtf32(209); returnValue = (ChrW(209)).ToString() End If End If End If If Not isTableB Then ' We are on table C, try to process 2 digits minCharPos = 2 minCharPos = IsNumber(value, charPos, minCharPos) If minCharPos < 0 Then ' OK for 2 digits, process it currentChar = Integer.Parse(value.Substring(charPos, 2)) currentChar = IIf(currentChar < 95, currentChar + 32, currentChar + 105) '' returnValue = returnValue & (ChrW(currentChar)).ToString() charPos += 2 Else ' We haven't 2 digits, switch to table B returnValue = returnValue & (ChrW(205)).ToString() isTableB = True End If End If If isTableB Then ' Process 1 digit with table B returnValue = returnValue & value.Substring(charPos, 1) charPos += 1 End If End While ' Calculation of the checksum checksum = 0 For [loop] As Integer = 0 To returnValue.Length - 1 currentChar = AscW(Char.Parse(returnValue.Substring([loop], 1))) currentChar = IIf(currentChar < 127, currentChar - 32, currentChar - 105) If [loop] = 0 Then checksum = currentChar Else checksum = (checksum + ([loop] * currentChar)) Mod 103 End If Next ' Calculation of the checksum ASCII code checksum = IIf(checksum < 95, checksum + 32, checksum + 105) ' Add the checksum and the STOP returnValue = returnValue & (ChrW(checksum)).ToString() & (ChrW(211)).ToString() End If End If Return returnValue End Function Private Shared Function IsNumber(ByVal InputValue As String, ByVal CharPos As Integer, ByVal MinCharPos As Integer) As Integer ' if the MinCharPos characters from CharPos are numeric, then MinCharPos = -1 MinCharPos -= 1 If CharPos + MinCharPos < InputValue.Length Then While MinCharPos >= 0 If AscW(Char.Parse(InputValue.Substring(CharPos + MinCharPos, 1))) < 48 OrElse AscW(Char.Parse(InputValue.Substring(CharPos + MinCharPos, 1))) > 57 Then Exit While End If MinCharPos -= 1 End While End If Return MinCharPos End Function Public Shared Function StringToBarcode39String(ByVal value As String, Optional ByVal addChecksum As Boolean = False) As String ' Parameters : a string ' Return : a string which give the bar code when it is dispayed with CODE128.TTF font ' : an empty string if the supplied parameter is no good Dim isValid As Boolean = True Dim currentChar As Char Dim returnValue As String = String.Empty Dim checksum As Integer = 0 If value.Length > 0 Then 'Check for valid characters For CharPos As Integer = 0 To value.Length - 1 currentChar = Char.Parse(value.Substring(CharPos, 1)) If Not ((currentChar >= "0"c AndAlso currentChar <= "9"c) OrElse (currentChar >= "A"c AndAlso currentChar <= "Z"c) OrElse currentChar = " "c OrElse currentChar = "-"c OrElse currentChar = "."c OrElse currentChar = "$"c OrElse currentChar = "/"c OrElse currentChar = "+"c OrElse currentChar = "%"c) Then isValid = False Exit For End If Next If isValid Then ' Add start char returnValue = "*" ' Add other chars, and calc checksum For CharPos As Integer = 0 To value.Length - 1 currentChar = Char.Parse(value.Substring(CharPos, 1)) returnValue += currentChar.ToString() If currentChar >= "0"c AndAlso currentChar <= "9"c Then checksum = checksum + AscW(currentChar) - 48 ElseIf currentChar >= "A"c AndAlso currentChar <= "Z"c Then checksum = checksum + AscW(currentChar) - 55 Else Select Case currentChar Case "-"c checksum = checksum + AscW(currentChar) - 9 Exit Select Case "."c checksum = checksum + AscW(currentChar) - 9 Exit Select Case "$"c checksum = checksum + AscW(currentChar) + 3 Exit Select Case "/"c checksum = checksum + AscW(currentChar) - 7 Exit Select Case "+"c checksum = checksum + AscW(currentChar) - 2 Exit Select Case "%"c checksum = checksum + AscW(currentChar) + 5 Exit Select Case " "c checksum = checksum + AscW(currentChar) + 6 Exit Select End Select End If Next ' Calculation of the checksum ASCII code If addChecksum Then checksum = checksum Mod 43 If checksum >= 0 AndAlso checksum <= 9 Then returnValue += (ChrW(checksum + 48)).ToString() ElseIf checksum >= 10 AndAlso checksum <= 35 Then returnValue += (ChrW(checksum + 55)).ToString() Else Select Case checksum Case 36 returnValue += "-" Exit Select Case 37 returnValue += "." Exit Select Case 38 returnValue += " " Exit Select Case 39 returnValue += "$" Exit Select Case 40 returnValue += "/" Exit Select Case 41 returnValue += "+" Exit Select Case 42 returnValue += "%" Exit Select End Select End If End If ' Add stop char returnValue += "*" End If End If Return returnValue End Function
For my first attempt, I installed the two fonts (and later rebooted when the fonts didn’t work and I realised this is still necessary as recent as Windows 7), and set to work on a demonstration in BIDS to see if I could use the fonts simply with the code provided to show and print barcodes.
It consists simply of:
- The custom code from above.
- A dummy data set (use anything) and a table with the detail grouping removed.
- Two expressions, one in a table cell each.
- Setting the font properties for each table cell to Code 3 de 9 and Code 128 respectively, 36pt (or any other arbitrary size) and resize the cell a little bigger to match.
And then when you export the report to PDF:
Wait a minute, that doesn’t look right!
What Went Wrong
This is where it gets complicated; the free Code 128 barcode font doesn’t work (as of RS2008 R2) when exporting to PDF. It’s supposed to work as it’s a TrueType font with embed rights and after checking the PDF properties it has embedded the correct font; it just doesn’t display properly in Adobe Reader (as of 10.0.1).
The way I got around this is to render the text using that font as an image then display it in an image control, which accepts a byte array representing the file from some more custom code. As it turns out other people have done this before, but it hasn’t been well explained:
Public Shared Function Code39(ByVal stringText As String) As Byte() Dim result As Byte() = Nothing Try result = GenerateImage("Code 3 de 9", StringToBarcode39String(stringText)) Catch ex As Exception End Try Return result End Function Public Shared Function Code128(ByVal stringText As String) As Byte() Dim result As Byte() = Nothing Try result = GenerateImage("Code 128", StringToBarcode128String(stringText)) Catch ex As Exception End Try Return result End Function Public Shared Function GenerateImage(ByVal fontName As String, ByVal stringText As String) As Byte() Dim oGraphics As System.Drawing.Graphics Dim barcodeSize As System.Drawing.SizeF Dim ms As System.IO.MemoryStream Using font As New System.Drawing.Font(New System.Drawing.FontFamily(fontName), 36) Using tmpBitmap As New System.Drawing.Bitmap(1, 1, System.Drawing.Imaging.PixelFormat.Format32bppArgb) oGraphics = System.Drawing.Graphics.FromImage(tmpBitmap) oGraphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.SingleBitPerPixel barcodeSize = oGraphics.MeasureString(stringText, font) oGraphics.Dispose() End Using Using newBitmap As New System.Drawing.Bitmap(barcodeSize.Width, barcodeSize.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb) oGraphics = System.Drawing.Graphics.FromImage(newBitmap) oGraphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.SingleBitPerPixel Using oSolidBrushWhite As New System.Drawing.SolidBrush(System.Drawing.Color.White) Using oSolidBrushBlack As New System.Drawing.SolidBrush(System.Drawing.Color.Black) oGraphics.FillRectangle(oSolidBrushWhite, New System.Drawing.Rectangle(0, 0, barcodeSize.Width, barcodeSize.Height)) oGraphics.DrawString(stringText, font, oSolidBrushBlack, 0, 0) End Using End Using ms = New System.IO.MemoryStream() newBitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png) End Using End Using Return ms.ToArray() End Function
I’ve now used two wrapper functions Code39 and Code128 which will do all the processing with the right font and return it as an image in a byte array. The steps to get this working, continuing on from before, are:
- Adding the above custom code to the report.
- Going into Report Properties -> References and adding the System.Drawing assembly to the report.
- Replacing the two textboxes with two image controls instead, with their settings as per below:
- It will also look far better to set the image sizing controls to Clip instead of Fit or FitProportional. For this reason you will probably want to experiment with the font sizes above and the longest barcode you’re going to reproduce so that it looks right without cutting anything off.
And surprise surprise, the screen renders similar to before and the PDF shows correctly too:
It took a lot of ugly code and work but I had the solution I was looking for, and now hopefully others can use it easily. I should make a few notes though:
- I wrote the part that generates the image, but I’m aware that there might be problems because it uses GDI+ which can leak non-managed Windows resources if they aren’t used properly. I wrapped everything in Using blocks which do an automatic .Dispose(), but I’m not a professional programmer and could have missed something.
- Using the plain fonts method showed much clearer barcodes than when drawning on an image. I also couldn’t work out why this is a case; I tried different types of font hints, and even changing the GDI+ code for the modern .NET DrawString function, and it only made things even worse.
If anyone ever works out a better plug-and-play way (not using custom assemblies or paid fonts), let me know.
Until next time,
After using this in the real world, I’ve found that the Code 39 generally scans okay as provided, but the Code 128 works better generated at 96pt, and then downscaled into the image box with “Fit”, so that it’s actually longer and thinner than it should be. Sounds crazy, but it works …
Be sure to do your own testing.