barcode-image

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 numbers 1 to 200. 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 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 UPC barcode. Source: Wikipedia

There are also two-dimensional barcodes, such as the now common QR Code1, among others.

garciacasa-qr-code

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, specifically PIL.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 characters
  • data is a string (str) or bytes 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 our convert 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, a 1-bit pixel has a range 0-1, an 8-bit pixel has a range 0-255, etc. Here, I use the 1 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:

base.png

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 an Image object. The .open() method opens and identifies the given image file.
  • Recall from earlier that the .convert() method takes a mode 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 the mode, size, and color parameters. We use RGBA mode, and retrieve the size of the base image by calling the size method. Note that the color 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 size 325.
  • ImageDraw.Draw() creates an objects for drawing in the given image. It can take two parameters: im, for the image to draw in, and mode (optional), which if omitted defaults to the given image mode. I passed base, the Image 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.

label-201

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.

anchors

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:

missing-num-label

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:

centered-anchor-text-label

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:

201-label

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.


  1. QR code is an abbreviation of Quick Response Code↩︎

  2. 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. ↩︎

  3. Here are the respective pages for the modules: Image, ImageDraw, and ImageFont. Always carefully read available documentation :) ↩︎

  4. doge  ↩︎

  5. 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↩︎

  6. Note that the anchor parameter is only supported by OpenType/TrueType fonts. The default (top left) alignment is used otherwise. ↩︎