The Problem
Recently, I needed three-digit numbers and their corresponding barcodes on 4x3 labels. I wanted somewhere in the range of one to three-hundred, all similar to the cover image of this post. Unsure how to proceed, I searched online for some ready-made solutions. As it happens, the web is lousy with “barcode generator” sites. Distractingly full of ads, the several I tried required the user to manually enter the data to be converted into a barcode. One at a time. And then proceed to download them - one at a time.
This was too slow and awkward a method. Although this may work fine for one-offs, I didn’t want to manually enter each number. I found some Excel/LibreOffice Calc solutions, but I wanted more customizability. I also came across some paid solutions, but I was not interested in that route. So, I figured I’d try my hand at solving the problem myself. Audere est facere.
My initial plan:
- write a program or script that would incrementally generate numbered barcodes based on a predefined interval. For example,
[1, 200]
would generate barcodes corresponding to numbers1
to200
. And then: - make a composite image with the generated barcode and the corresponding number.
Simple enough. Yet before going over this project in depth, first we need a primer on barcodes.
A Brief History of Barcodes
Most people are familiar with barcodes, usually from shopping, or owning virtually any product. In short, barcodes are a representation of data (such as numbers, letters, and other characters) in visual, machine-readable form. A symbology is a mapping between messages (data) and barcodes. A variety of symbologies exist and are in use today.
First generation linear barcodes, or one-dimensional barcodes consist of lines and spaces of varying widths.
garcia.casa in code128 symbology
The Universal Product Code is a widely used barcode symbology for trade products. UPC is made up of 12 digits representing a unique item. These are what you typically see in department and grocery stores.
UPC barcode. Source: Wikipedia
There are also two-dimensional barcodes, such as the now common QR Code1, among others.
But the notion of data encoded in machine-readable format goes back to the use of punch cards, such as those used by Joseph Jacquard’s loom. Punch cards are something like a primitive form of barcodes, using binary information for the storage and retrieval of data.
Fast forward to 1972, when the UPC is developed at IBM. One year later, it is adopted by the grocery industry. Now, integrated QR code readers are default on most new phones.
Code 128
For my purposes, I used the linear barcode symbology Code 128. It’s a higher density barcode, which was important because of the space constraints of the project.
treepoem
To create the actual barcodes, I used a python renderer called treepoem. Treepoem wraps the Barcode Writer in Pure PostScript (BWIPP) library and the Ghostscript command line tool. It has an easy-to-use API that made this part of my project much easier.
Installation
My dev machine is running Ubuntu 20.04, and so all of the instructions that follow are written in terms of that OS. Presumably they work on other versions and Debian, but YMMV.
Note: It’s recommended to create a virtual environment before installing any new python packages. For instance,
python -m venv barcode-env
.
First, I needed to make sure Ghostscript
was installed on my dev system:
sudo apt install -y ghostscript
Then, I installed treepoem
using pip:
python -m pip install treepoem
Basic Usage
After reading over some of the treepoem documentation, I could easily create barcodes. Below is the python snippet calling the API to generate the garcia.casa QR Code.
# garcia-qr-code.py
import treepoem
image = treepoem.generate_barcode(
barcode_type = "qr-code",
data = "garcia.casa",
)
image.convert("1").save("garciacasa-qr-code.png")
And here is the snippet for the Code 128 barcode for garcia.casa:
# garcia-code128.py
import treepoem
image = treepoem.generate_barcode(
barcode_type = "code128",
data = "garcia.casa",
)
image.convert("1").save("garciacasa-code128.png")
Let’s take a closer look and analyze the Code 128 snippet. First, the image
object is created:
image
is a PIL image file object, specificallyPIL.EpsImagePlugin.EpsImageFile
- The
generate_barcode
method creates the barcode barcode_type
takes the name of the barcode type, in our case code128, which can encode all 128 ASCII charactersdata
is a string (str
) orbytes
of data that is encoded into the barcode
Next, we call the convert
method. From the pillow documentation, this method returns a copy of this image.
# method signature
Image.convert(mode=None, matrix=None, dither=None, palette=0, colors=256)
We pass the
mode
argument to ourconvert
method. The mode of an image is a string which defines the type and depth of a pixel in a given image. Every pixel uses the full range of the bit depth. In other words, a1-bit
pixel has a range0-1
, an8-bit
pixel has a range0-255
, etc. Here, I use the1
mode: 1-bit pixels — two colors, black and white — stored with one pixel per byte. 1-bit color images are also known as a binary image. 2
Lastly, we call the save
method on our image, which saves the image under the provided filename, garciacasa-code128.png
.
I’ll Take 200, Please
Now that I could generate a barcode for a given number, I needed to create a range of barcodes. Specifically, barcodes representing every number between 200 and 400, inclusive. More formally: *for every x Є [200,400], create a barcode in Code 128 representing x*. In pseudocode, this would be something like:
for each x in [200,400]
barcode = generate_barcode(x)
save(barcode)
And after translating to python, we get:
# generate_barcodes.py
import treepoem
for i in range(200, 401):
barcode = treepoem.generate_barcode(
barcode_type = "code128",
data = str(i),
)
filename = str(i) + ".png"
barcode.convert("1").save(filename)
I ran python generate_barcodes.py
and it worked as intended. The barcodes/
directory now contains all the barcode images. Below is the full, cleaned-up version of generate_barcodes.py
.
import treepoem
def generate_barcodes(x, y):
for i in range(x, y):
barcode = treepoem.generate_barcode(
barcode_type = "code128",
data = str(i),
)
filename = "barcodes/" + str(i) + ".png"
barcode.convert("1").save(filename)
print("Barcode " + str(i) + " created")
print("Barcode generation complete!")
def main():
x = 200
y = 401
generate_barcodes(x, y)
main()
Adding Numbers
Next, I needed to create new images using the newly generated barcodes and their corresponding numbers. My plan was as following:
- iterate through all the barcodes in the
barcodes
directory - Using Pillow, the fork of the Python Imaging Library (PIL), create a new base image the scale of the labels (4x3)
- Add text to the base image, specifically the current number associated with its barcode
- Add the current barcode image to the base image
- Save the new composite image
Pillow: Python Imaging Library (PIL)
From the PIL
library, I needed to import the Image
, ImageDraw
, and ImageFont
modules3 into my script. But first, I created a blank white image using GNU Image Manipulation Program (GIMP) with dimensions of 600x400 pixels. I exported the image from GIMP as base.png
.
Behold:
Impressive4, I know.
Next, I created Image
, ImageFont
, and ImageDraw
objects for my text:
# add_nums.py
from PIL import Image, ImageDraw, ImageFont
with Image.open("base.png").convert("RGBA") as base:
# load base image, font, and draw objects
text = Image.new("RGBA", base.size, (255, 255, 255, 0))
font = ImageFont.truetype("usr/share/fonts/truetype/ubuntu/Ubuntu-B.ttf", 325)
draw = ImageDraw.Draw(text)
Let’s examine the above more closely:
Image.open()
returns anImage
object. The.open()
method opens and identifies the given image file.- Recall from earlier that the
.convert()
method takes amode
parameter defining the type and depth of a pixel in the given image. Here, we use"RGBA"
mode5 (4x8-bit pixels, true color with transparency mask). Image.new()
create a new image. It takes themode
,size
, andcolor
parameters. We useRGBA
mode, and retrieve the size of the base image by calling thesize
method. Note that thecolor
argument takes RGBA values.ImageFont.truetype()
loads a TrueType or OpenType font file (or file-like object), and creates a font object. I used Ubuntu-B.ttf from my dev machine, with font size325
.ImageDraw.Draw()
creates an objects for drawing in the given image. It can take two parameters:im
, for the image to draw in, andmode
(optional), which if omitted defaults to the given image mode. I passedbase
, theImage
object, to this method.
Then, as shown below, I added the number text and created an alpha_composite image using base
and text
. The next step was to paste the barcode image.
Alpha Composite and Text Alignment
...
# add_nums.py
# add text
draw.text((0, 0), "201", font=font, fill="black")
# output alpha composite image
out = Image.alpha_composite(base, text)
# open and paste barcode image onto alpha composite image
bc = Image.open("barcodes/201.png")
out.paste(bc)
out.save("label-201.png")
...
After saving the file and running python add_nums.py
, I open label-201.png and get the following result.
Not quite right. That’s because the draw.text()
method from above (in add_nums.py
) takes as a first argument a 2-tuple, xy
, as the anchor coordinates of the text. It can also take an anchor
parameter6, a two character string, which determines the alignment of drawn text relative to the given xy
coordinates. The default alignment is the top left.
So, I want the anchor
alignment to be in the center and at the bottom. Per the documentation, I should use the -m
(middle) and -s
(baseline) alignments, or simply -ms
. I used the below picture as a reference.
Here is the modified python:
...
# add_nums.py
# add text
draw.text((0, 0), "201", anchor="ms", font=font, fill="black")
# output alpha composite image
out = Image.alpha_composite(base, text)
# open and paste barcode image onto alpha composite image
bc = Image.open("barcodes/201.png")
out.paste(bc)
out.save("label-201.png")
...
And the new image:
Now the number itself is missing. Why? Well, I left the anchor coordinates the same, (0, 0)
, and need to adjust them accordingly. Basically, I want the x
coordinate to be half the length of the width of my base image, and the y
coordinate to be slightly below the top of the image, say 10 pixels. That is,
- for
x
, I’ll use x_text = 0.5 * base.width - for
y
, I’ll use y_text = base.height - 10
Note that the anchor coordinates need an integer value, so I’ll type cast x_text
to int
. The modified python:
# add_nums.py
...
# get the (x,y) anchor coordinates of the text
x_text = int(0.5 * base.width)
y_text = base.height - 10
# add text
draw.text((x_text, y_text), "201", anchor="ms", font=font, fill="black")
...
And the new image:
Barcode Alignment
Much better. The text alignment looks great. At this point, the last step was to align the barcode.
The .paste()
method takes an optional 4-tuple giving the region to paste into. One can also use a 2-tuple, it’s treated as the upper left corner[^7]. I chose to use a 2-tuple so I could take advantage of the bc.width
and bc.height
properties of the barcode image.
For my x coordinate of the 2-tuple, I wanted half width of the base image minus half the width of the barcode image. For the y coordinate, I wanted something like 5 pixels. Modifying my python yet again:
...
# open and paste barcode image onto alpha composite image
bc = Image.open("barcodes/201.png")
x_bc = x_text - int(0.5 * bc.width)
y_bc = 5
out.paste(bc, (x_bc, y_bc))
out.save("label-201.png")
...
Running python add_nums.py
now produces the final image:
Last Steps
Now that I had the desired output, I needed to add a for loop to generate all the final barcode/number images.
# add_nums.py
from PIL import Image, ImageDraw, ImageFont
with Image.open("base.png").convert("RGBA") as base:
for i in range(200, 401):
# load base image, font, and draw objects
text = Image.new("RGBA", base.size, (255, 255, 255, 0))
font = ImageFont.truetype("/usr/share/fonts/truetype/ubuntu/Ubuntu-B.ttf", 325)
draw = ImageDraw.Draw(text)
x_text = int(0.5 * base.width)
y_text = base.height - 10
# add text
draw.text((x_text, y_text), str(i), anchor="ms", font=font, fill="black")
# output alpha composite image
out = Image.alpha_composite(base, text)
# open and paste barcode image onto alpha composite image
bc = Image.open("barcodes/" + str(i) + ".png")
x_bc = x_text - int(0.5 * bc.width)
y_bc = 5
out.paste(bc, (x_bc, y_bc))
out.save(str(i) + "-label.png")
As a final step, I create a add_numbers
function in my generate_barcodes.py
file, and called it from the main()
function. The full code is below.
Final Product
Update: I’ve added basic user interaction to the final script. After starting the program, the user inputs the first and last flag numbers to be generated. Again, it’s very basic, yet is better than manually hard coding the values.
import treepoem
from PIL import Image, ImageDraw, ImageFont
def generate_barcodes(x, y):
"""
Generates a set of natural number barcode images.
A range of barcodes are generated between x and y,
where x < y.
"""
for i in range(x, y):
barcode = treepoem.generate_barcode(
barcode_type = "code128",
data = str(i),
)
filename = "barcodes/" + str(i) + ".png"
barcode.convert("1").save(filename)
print ("Barcode " + str(i) + " created")
print ("Barcode generation complete.")
def add_numbers(x, y):
"""
Generates final image with barcode and corresponding number.
A range of images with barcodes and their corresponding natural number
are generated between x and y, where x < y.
"""
with Image.open("base.png").convert("RGBA") as base:
for i in range(x, y):
# load base image, font, and draw objects
font_path = "/usr/share/fonts/truetype/ubuntu/"
font_type = "/usr/share/fonts/truetype/abyssinica/AbyssinicaSIL-Regular.ttf"
font_size = 350
text = Image.new("RGBA", base.size, (255, 255, 255, 0))
font = ImageFont.truetype(font_path + font_type, font_size)
draw = ImageDraw.Draw(text)
x_text = int(0.5 * base.width)
y_text = base.height - 20
# add text
draw.text((x_text, y_text), str(i), anchor="ms", font=font, fill="black")
# output alpha composite image
out = Image.alpha_composite(base, text)
# open and paste barcode image onto alpha composite image
bc = Image.open("barcodes/" + str(i) + ".png")
x_bc = x_text - int(0.5 * bc.width)
y_bc = 30
out.paste(bc, (x_bc, y_bc))
final_image = "labels/" + str(i) + "-label.png"
out.save(final_image)
print("Composite image created: " + final_image)
def main():
active = True
output = ""
while active:
try:
first = int(input("Please enter the first number: "))
last = int(input("Please enter the last number: "))
if first > last:
error = "The first number must be less than or equal to second. Please try again."
print(error)
else:
output = "Numbers and barcodes to be generated: %d to %d"%(first, last)
active = False
except ValueError:
print("You must enter integers! Please try again.")
print(output)
x = first
y = last + 1
generate_barcodes(x, y)
print("Creating composite images...")
add_numbers(x, y)
main()
Conclusion
Something I would like to do: make this script interactive[^9], so a user can input the lower and upper bounds of the range for barcodes, and also add support for custom alpha-numeric labels as well. I’m sure logistics software already does all of this much more competently, but it was fun nonetheless.
Be sure to reach out if you have any questions, comments, or criticisms.
QR code is an abbreviation of Quick Response Code. ↩︎
For those interested, a binary image ‘can be interpreted as a subset of the two-dimensional integer lattice Z2’, also known as the |“square” lattice. ↩︎
Here are the respective pages for the modules: Image, ImageDraw, and ImageFont. Always carefully read available documentation :) ↩︎
… ↩︎
RGBA
stands for red green blue alpha, because it is a three-channel, additive RBG color model with a supplemental fourth channel, the alpha channel. The alpha channel determines the opaqueness of each pixel, allowing for techniques like alpha compositing. ↩︎Note that the
anchor
parameter is only supported by OpenType/TrueType fonts. The default (top left) alignment is used otherwise. ↩︎