Thursday, November 19, 2009

Seeing Stripes: Experiment with ZXing

I recently took a look at the ZXing Project which is described as "an open-source, multi-format 1D/2D barcode image processing library implemented in Java.". While its primary focus is mobile phones, I decided to look at the javase component of the project. ZXing also comes with a servlet. But I first wanted to see how the lower level components worked.


Setup
Not seeing any pre-compiled jars, I downloaded the latest sources (ZXing-1.4.zip). After loading it into Eclipse, and poked around a bit to figure out how to decode barcode images of different types. Finally, I used the pre-made ant files to create the two jars I needed: core.jar and javase.jar.

Decoding barcode images
While the project has some decent documentation, the developer notes seem to be outdated in a few places. But examining some of the javase classes turned up a convenience class called MultiFormatReader. From what I could tell it provides a simple way to decode barcodes when you may not know the type ahead of time. Obviously if you know the barcode type ahead of time, it is more efficient to use the specific reader for that type. But since I wanted to test all supported types the MultiFormatReader was perfect for my needs.

Now before I could test ZXing, I needed some barcode images. So I used CFBarbecue.cfc to generate a sample of all of the barcodes supported by the Barbecue library. Now ZXing does not support all of them. But there is a good intersection between the two. Once I had my images, I created an instance of MultiFormatReader, using the ever-handy JavaLoader.cfc. I also passed the reader some hints to increase accuracy.

Note: This code is for demonstration purposes only. Normally, the javaLoader should be stored in the server scope to avoid memory leaks.

<cfset paths = [] />
<cfset arrayAppend(paths, expandPath('zxing-1.4/core/core.jar')) />
<cfset arrayAppend(paths, expandPath('zxing-1.4/javase/javase.jar')) />
<cfset loader = createObject("component", "javaloader.JavaLoader").init( paths ) />

<!--- Create hints for the reader. TRY_HARDER increases accuracy --->
<cfset DecodeHintType = loader.create("com.google.zxing.DecodeHintType") />
<cfset hints = loader.create("java.util.Hashtable").init() />
<cfset hints.put(DecodeHintType.TRY_HARDER, javacast("boolean", true)) />

<!--- Since we will be reading multiple images, create one reader and reuse it  --->
<cfset Reader = loader.create("com.google.zxing.MultiFormatReader").init() />
<cfset Reader.setHints(hints) />

Next, I used cfdirectory to grab a list of all of my barcode images. Then looped through the images and attempted to decode each one. Now in order to use the reader's decode method, I needed to convert my image to a compatible format: BinaryBitmap. So I read in each image, extracted the underlying BufferedImage and passed it through a few of the ZXing classes designed to prepare images for decoding.

Finally, I called the reader's decode method to decode the image. If the image was successfully decoded it returns a Result object containing some useful properties like the raw text and barcode type.

One thing to be aware of is that error handling is unlikely to yield any useful information, beyond the fact that an error occurred. From the notes in the current source code, the way in which decoding errors are handled is undergoing a transformation, to increase performance. So for now do not expect much exception information beyond name, rank and serial number.


<!--- read all barcode images in my subdirectory --->
<cfdirectory action="list" directory="#ExpandPath('images')#"
            filter="*.gif"
            name="barcodeImages" />

<!---
    Try and decode each barcode with z-xing.
    Note: Not all codes are supported by z-xing!!!
--->
<cfoutput query="barcodeImages">
    <cfset imagePath = Directory &"/"& name />
    <cftry>

        <!--- display the original image --->
        <strong>Barcode Image</strong> #name# <br />
        <cfimage action="writeToBrowser" source="#imagePath#"><br />

        <!--- extract the BufferedImage of the current barcode --->
        <cfset img = ImageGetBufferedImage(ImageRead(imagePath)) />

        <!--- prepare the image for decoding --->
        <cfset source = loader.create("com.google.zxing.client.j2se.BufferedImageLuminanceSource").init(img) />
        <cfset binarizer = loader.create("com.google.zxing.common.GlobalHistogramBinarizer").init(source) />
        <cfset bitmap = loader.create("com.google.zxing.BinaryBitmap").init(binarizer) />

        <!--- This method reuses the existing settings for decoding multiple images --->
        <cfset decodedResult = Reader.decodeWithState(bitmap) />

        <!--- display the results --->
        <strong>Decoded Text</strong> #decodedResult.getText()# <br />
        <strong>Decoded Format</strong> #decodedResult.getBarcodeFormat()# <br />

        <cfcatch>
            Not supported or unable to decode: #cfcatch.type# #cfcatch.message#
        </cfcatch>
    </cftry>
    <br /><br /><hr />
</cfoutput>




All of my sample images were decoded except:

  • 2of7
  • Codabar
  • Int2of5
  • Monarch
  • NW7
  • PDF417
  • PostNet
  • Std2of5
  • USD4
While I knew some of them are not supported, but I was surprised that the PD417 and Interleaved 2 of 5 samples failed. I am not yet sure why. For now I will assume it was an error on my part. I will have to investigate the individual readers further.

However, I was able to decode a DataMatrix barcode using the DataMatrixReader. (Created using the free servlet at IDAutomation.com). Just be aware the current project documentation lists DataMatrix decoding as "alpha quality". So do not expect miracles.




<cfset imagePath = "c:\barCode_loremIpsum_idAutomation.gif" />

<cfset DecodeHintType = loader.create("com.google.zxing.DecodeHintType") />
<cfset hints = loader.create("java.util.Hashtable").init() />
<cfset hints.put(DecodeHintType.PURE_BARCODE, javacast("boolean", true)) />

<!--- prepare the image for decoding --->
<cfset img = ImageGetBufferedImage(ImageNew(imagePath)) />
<cfset source = loader.create("com.google.zxing.client.j2se.BufferedImageLuminanceSource").init(img) />
<cfset binarizer = loader.create("com.google.zxing.common.GlobalHistogramBinarizer").init(source) />
<cfset bitmap = loader.create("com.google.zxing.BinaryBitmap").init(binarizer) />

<!--- attempt to decode the image --->
<cfset Reader = loader.create("com.google.zxing.datamatrix.DataMatrixReader") />
<cfset decodedResult = Reader.decode( bitmap, hints ) />

<cfoutput>
<!--- display the original image --->
<strong>Barcode Image</strong> #imagePath# <br />
<cfimage action="writeToBrowser" source="#imagePath#"><br />

<!--- display the results --->
<strong>Decoded Result</strong><br />
#decodedResult.getText()# <hr />
</cfoutput>

Other Classes
The j2se component also has a few classes you might find useful for quick tests. The first is GUIRunner. It is a very rudimentary GUI you can use to test a single bar code image. To use it, simply open a command prompt and enter the following. Just modify the "-cp" value (classpath) to point to the location of your jars.

Example:


C:\test\zxing> java -cp c:\test\zxing\core.jar;c:\test\zxing\javase.jar com.google.zxing.client.j2se.GUIRunner


The other class is CommandLineRunner. It can be used to decode a single image, a directory of images or a url. Just supply the locations of your jars and the file/directory/url you wish to decode. The "--try_harder" hint can be used to increase accuracy, and the "--dump_results" option to save the results to a file. Note: The results of each barcode are saved to individual files. So keep that in mind if you are decoding an entire directory.

Example:

C:\test\zxing> java -cp c:\test\zxing\core.jar;c:\test\zxing\javase.jar com.google.zxing.client.j2se.CommandLineRunner c:/test/zxing/barcodeimages --try_harder


Well, that is it for now. I will have to experiment with some lower quality images, to see how the results stack up in a real test. (Once I find a supported phone ...)

8 comments:

dswitkin November 22, 2009 at 6:16 PM  

Very nice article, thanks! I believe we have PDF417 support turned off by default. You will either need to request it using the hints field, or uncomment MultiFormatReader.java line 133. However it's pretty basic - I don't believe it supports error correction yet, for example.

You're also completely right about the docs, we do need to bring them up to date.

Cheers,
Daniel Switkin

cfSearching November 22, 2009 at 8:50 PM  

@Daniel,

Thanks for the tip about PDF417. With my images I also needed to adjust the quiet space. But then it decoded as expected. (Though I am keeping in mind the current support is basic).

BTW - The library was very easy to use. So I look forward to future versions. Keep up the good work!

Cheers,
Leigh

Jon March 17, 2010 at 8:49 AM  

Thanks for documenting your experiences w/xzing, barbecue and javaloader. I've been able to implement a couple of cfc functions that write image files and extract barcoded data. I'd like my writer function to return the binary image, which I do by using the java.io.File class to write it to disk - then reading back with cffile action='readbinary'. Is there a more direct solution?

cfSearching March 18, 2010 at 2:19 PM  

Hi @Jon,

With Barbecue? Yes, just save the image data to a ByteArrayOutputStream (ie an array of bytes instead of a file). There is an example here.

(2) Writing Images to an OutputStream

-Leigh

Jon March 18, 2010 at 6:58 PM  

Very clean solution, thanks for pointing that out Leigh.

Rod,  June 23, 2010 at 12:48 PM  

Ok, total noob question but what program did you use to convert the ant files to jar?

cfSearching June 24, 2010 at 6:25 AM  

@Rod,

I used Eclipse with an Ant plugin, which makes it extremely simple. Just a "run ant task" and presto, instant jars.

-Leigh

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep