Sniff is a "Scratch-like" programming language that's designed to help Scratchers move gently from Scratch to more conventional languages. They can start writing programs, without having to learn a new language because Sniff is based on Scratch. They learn a little more about variables, compiling, syntax errors (!), and they can have fun controlling real hardware while they're doing it.

Wednesday, 22 March 2017

Putting the bars in barcodes

For some reason I was thinking about barcodes... I'm not sure why, but I started wondering if we could make them in Sniff... turns out you can!

A traditional barcode that you find on a tin of baked beans (technically a UPC-A barcode) represents 12 digits. This first 6 are the manufacturers code, assigned to a company (which applies, and gets given its own number), and the last 5 which represent a specific item, as chosen by the manufacturer:

make manufacturerID string "097855"
make itemID string "08839"

make barcodeValue string

But wait a minute - that's only 11. The final digit is a checksum. When the barcode is printed the final digit is calculated from the other 11, and added to the end. When its read the same sum is calculated by the reader, and if the final digit doesn't match it knows something's gone wrong and it can reject the code.

when start

.if not length of manufacturerID = 6
..say "Bad Manufacturer ID"
..stop all
.if not length of itemID = 5
..say "Bad Item ID"
..stop all
.set barcodeValue to join manufacturerID itemID
.broadcast makeChecksum and wait
.say barcodeValue

Here's the beginnings of the main script. We check that the manufacturer and item ID's are the correct length and join them together to make the beginnings of the barcodeValue - the number that is printed along the bottom. You'll note that they're all strings, as the barcode isn't really a number - its more a sequence of digits, and its easier to handle them as strings.

when makeChecksum
.make counter number
.make sum number
.set sum to 0
.set counter to 1
.repeat 6
..change sum by value of letter counter of barcodeValue
..change counter by 2
.set sum to sum*3
.set counter to 2
.repeat 5
..change sum by value of letter counter of barcodeValue
..change counter by 2
.repeat until sum < 11
..change sum by -10
.set sum to 10-sum
.set barcodeValue to join barcodeValue [sum]

To calculate the checksum, we add up the odd placed digits and multiply by 3. Then we add the even placed digits. Once we have the sum we need to find the number to make the sum add up to a multiple of 10. i.e. if the sum is 74 then the final answer is 6, which would make it up to 80. There are a few ways of doing that, but the simplest is to keep subtracting 10 until its less that 11 (i.e. its now 1...10), then subtract it from 10. This is the checksum value and we tag it on the end of the barcodeValue.

Using our example code, the checksum is a 0, so we get:


If you look closely at a bar code you'll notice that it has lines and spaces of varying thickness. In fact both the lines and spaces can be between 1 and 4 units wide. Each digit is represented by two lines and two spaces. To store that information we make a list called symbols:

make symbols list of strings

when initSymbols
.add "3211" to symbols
.add "2221" to symbols
.add "2122" to symbols
.add "1411" to symbols
.add "1132" to symbols
.add "1231" to symbols
.add "1114" to symbols
.add "1312" to symbols
.add "1213" to symbols
.add "3112" to symbols

These represent the digits 0 to 9.

make barcodeWidths string

when makeBarWidths

.make counter number
.make index number
.make symbol string
.set barcodeWidths to ""
.set barcodeWidths to "111 "
.set counter to 1
.repeat 6
..set index to value of letter counter of barcodeValue
..change index by 1
..set symbol to item index of symbols
..set barcodeWidths to join barcodeWidths symbol
..set barcodeWidths to join barcodeWidths " "
..change counter by 1
.set barcodeWidths to join barcodeWidths "11111 "
.repeat 6
..set index to value of letter counter of barcodeValue
..change index by 1
..set symbol to item index of symbols
..set barcodeWidths to join barcodeWidths symbol
..set barcodeWidths to join barcodeWidths " "
..change counter by 1
.set barcodeWidths to join barcodeWidths "111 "

A barcode begins with two lines 1 unit thick, with a 1 unit space between them, which is represented as 111. Then we add the symbols for the first 6 digits, so if the first digit was a 9 then we'd add 3112, meaning a 3 unit line, a 1 unit space, a 1 unit line and a 2 unit space.

In the middle of every barcode is the code 11111, representing 3 thin lines. We then continue on to the end of the barcodeValue, and then tag a "111" end sequence on.

111 3211 3112 1312 1213 1231 1231 11111 3211 1213 1213 1411 3112 3211 111 

In the case of our test code, that gives the above string. You'll note that program adds some extra spaces to separate each digit. These won't appear in the final barcode, but they're handy for checking that everything is working.

The next step is to turn those widths in to actually bars.

when makeBars

.make counter number
.make width number
.make isLine  boolean
.set isLine to yes
.set barcode to ""
.repeat length of barcodeWidths using counter
..if not letter counter of barcodeWidths = " "
...set width to value of letter counter of barcodeWidths
...repeat width
....if isLine
.....set barcode  to join barcode "|"
.....set barcode  to join barcode   " "
...set isLine to not isLine

We simply loop over the length of the barcodeWidths string we just made and get the value of the digit. If its a line we print a vertical bar, otherwise we print a space. The number of lines or bars we print is the width of the line.

| |   || |   | || ||| || || ||| ||   | ||   | | | |||  | |  |   |  |   |    | ||| |  |||  | | |

And there it is - our barcode! It's not exactly pretty and its hard to read, because a thick lines appear as multiple vertical bars, when they should be solid, but trust me its the right answer...

make nativeFile device
make fileData string

make image bitmap device
make displayColor number
make displayX number
make displayY number

when drawBarcode
.set displayY to 40
.set displayX to length of barcode
.tell image to "new"
.repeat length of barcode using displayX
..if letter displayX of barcode="|"
...set displayColor to 000
...set displayColor to 777
..repeat 40 using displayY
...tell image to "set pixel"
.set fileData to "barcode.bmp"
.tell image to "save"

Of course what we really should do is draw our barcode as a bitmap and save it to disk. Fortunately that turns out it be really easy - just go through the barcode and if its a "|" draw a line of black pixels. If its a space draw a line of white pixels. Once we're done, we save it to disk.

[tech note: currently on Windows, the Bitmap device requires that you also make a Window device - otherwise it gets confused that you want to make an image, without a way to ever display it! There's a fix in the next release]

And here it is!!! A perfect barcode, which looks just like the one on the box I took the original number from. Now all that's left to do is print out a bunch of these on sticky labels and go cause havoc in the school library! (actually books use a slightly different format!).

What's really nice about this project is that it would make a really great group project. It breaks down really neatly into 4 parts of similar difficulty, which all need to work to make the whole thing happen. On pair of students is given the ID's and has to make the barcode value with checksum. The second has to turn that into line widths, the next turn that into ascii bars, and the final pair draws the bars. As we have a worked example you can provide each pair with an example of the input they need to read, and the output they need to generate, so they can all work at the same time, and then when they know each part is working, they can bring them all together!

No comments:

Post a Comment