Sam
Sam

Reputation: 193

Plotting barcode displays differently in plot window than in saved .pdf

I am having some trouble saving my pdf properly. I am trying to plot a barcode label and subsequently save it as a pdf, as in the following code. I have installed the code128.ttf font on my windows. Also, I have tried setting the .savefig dpi argument to fig.dpi, as argued in this post.

import os

import matplotlib.pyplot as plt

from matplotlib import font_manager as fm


def draw_label(label, label_dimensions_x=3.8189, label_dimensions_y=1.41732):

    # import barcode code128 font
    fpath = os.path.join("path", "to", "font", "code128.ttf")

    prop = fm.FontProperties(fname=fpath, size=58)

    fig, ax = plt.subplots(1, figsize=(label_dimensions_x,
                                       label_dimensions_y))

    plt.axis('off')
    plt.xticks([], [])
    plt.yticks([], [])
    plt.tight_layout()
    plt.xlim(0, label_dimensions_x)
    plt.ylim(0, label_dimensions_y)

    # plot barcode
    plt.text(label_dimensions_x / 2, label_dimensions_y / 2, label,
             ha='center', va='bottom',
             fontproperties=prop)

    plt.show()

    try:
        plt.savefig(os.path.join("path", "to", "output", label + '.pdf'),
                    dpi=plt.gcf().dpi)
    except PermissionError:
        logging.warning("Close the current label pdf's before running this script.")

    plt.close()

    return

draw_label('123456789')

This is what is output in the plot window.

This is what is output in the .pdf saved file, and this happens for all kinds of labels - it's not as if the numbers 1 to 9 except 8 are not printable. EDIT: If I substitute a normal text font (in this case Frutiger Roman) for the code128.ttf, and set plt.axis('on') the text is not clipped, see this. Admitted, it's not pretty and doesn't fit too well, but it should be readable still.

Upvotes: 2

Views: 598

Answers (2)

Mark Ransom
Mark Ransom

Reputation: 308120

You're over-complicating things by using matplotlib and a font. Generating an image directly and saving it to a PDF file is not much more complicated, and far more reliable.

As noted by Brian Anderson, it's not enough to encode the characters in your string. You need to add a start code, a checksum, and a stop code to make a complete barcode. The function code128_codes below does this, leaving the conversion to an image as a separate step.

from PIL import Image

def list_join(seq):
    ''' Join a sequence of lists into a single list, much like str.join
        will join a sequence of strings into a single string.
    '''
    return [x for sub in seq for x in sub]

_code128B_mapping = dict((chr(c), [98, c+64] if c < 32 else [c-32]) for c in range(128))
_code128C_mapping = dict([(u'%02d' % i, [i]) for i in range(100)] + [(u'%d' % i, [100, 16+i]) for i in range(10)])

def code128_codes(s):
    ''' Code 128 conversion to a list of raw integer codes.
        Only encodes ASCII characters, does not take advantage of
        FNC4 for bytes with the upper bit set. Control characters
        are not optimized and expand to 2 characters each.
        Coded for https://stackoverflow.com/q/52710760/5987
    '''
    if s.isdigit() and len(s) >= 2:
        # use Code 128C, pairs of digits
        codes = [105] + list_join(_code128C_mapping[s[i:i+2]] for i in range(0, len(s), 2))
    else:
        # use Code 128B and shift for Code 128A
        codes = [104] + list_join(_code128B_mapping[c] for c in s)
    check_digit = (codes[0] + sum(i * x for i,x in enumerate(codes))) % 103
    codes.append(check_digit)
    codes.append(106) # stop code
    return codes

_code128_patterns = '''
    11011001100 11001101100 11001100110 10010011000 10010001100 10001001100
    10011001000 10011000100 10001100100 11001001000 11001000100 11000100100
    10110011100 10011011100 10011001110 10111001100 10011101100 10011100110
    11001110010 11001011100 11001001110 11011100100 11001110100 11101101110
    11101001100 11100101100 11100100110 11101100100 11100110100 11100110010
    11011011000 11011000110 11000110110 10100011000 10001011000 10001000110
    10110001000 10001101000 10001100010 11010001000 11000101000 11000100010
    10110111000 10110001110 10001101110 10111011000 10111000110 10001110110
    11101110110 11010001110 11000101110 11011101000 11011100010 11011101110
    11101011000 11101000110 11100010110 11101101000 11101100010 11100011010
    11101111010 11001000010 11110001010 10100110000 10100001100 10010110000
    10010000110 10000101100 10000100110 10110010000 10110000100 10011010000
    10011000010 10000110100 10000110010 11000010010 11001010000 11110111010
    11000010100 10001111010 10100111100 10010111100 10010011110 10111100100
    10011110100 10011110010 11110100100 11110010100 11110010010 11011011110
    11011110110 11110110110 10101111000 10100011110 10001011110 10111101000
    10111100010 11110101000 11110100010 10111011110 10111101110 11101011110
    11110101110 11010000100 11010010000 11010011100 1100011101011'''.split()

def code128_img(s, height=100, bar_width=1):
    ''' Generate a Code 128 barcode image.
        Coded for https://stackoverflow.com/q/52968042/5987
    '''
    codes = code128_codes(s)
    pattern = ''.join(_code128_patterns[c] for c in codes)
    pattern = '00000000000' + pattern + '00000000000'
    width = bar_width * len(pattern)
    color, bg = (0, 0, 0), (255, 255, 255)
    im = Image.new('RGB', (width, height), bg)
    ld = im.load()
    for i, bar in enumerate(pattern):
        if bar == '1':
            for y in range(height):
                for x in range(i * bar_width, (i + 1) * bar_width):
                    ld[x, y] = color
    return im

>>> im = code128_img('AM-H-10-01-1')
>>> im.save(r'c:\temp\temp.pdf')

Upvotes: 0

Brian Anderson
Brian Anderson

Reputation: 1766

Sam,

First, your barcode won't scan, as is. The string requires a start character, a checksum and a stop character to be added for Code128B. So, there's that.

enter image description here

I recommend changing to Code 39 font (which, doesn't require checksum, and start and stop characters are the same: "*") or writing the code to produce the checksum and learning a little more about Code 128 at Code 128 Wiki.

Second, I suspect there are issues with the bounding box for the graphic during the conversion to PDF. That small section of barcode being converted looks more like a piece of the number nine in the string. I suspect there is some image clipping going on. enter image description here

Try substituting a regular text font to make sure the barcode image isn't being lost in the conversion.

Edited answer to include suggestion to use PNG instead of PDF.

I managed to get the software to work if you output to PNG format. I know, now the problem becomes how to convert PNG to PDF. You can start by investigating some of the libraries mentioned here: Create PDF from a list of images

In short I recommend you create graphics files and then embed them in document files.

I also added the code you need to build the barcode with the start, checksum and stop characters:

import os

import matplotlib.pyplot as plt

from matplotlib import font_manager as fm

def draw_label(label, label_dimensions_x=3.8189, label_dimensions_y=1.41732):

    # import barcode code128 font
    fpath = os.path.join("./", "code128.ttf")

    prop = fm.FontProperties(fname=fpath, size=32)

    fig, ax = plt.subplots(1, figsize=(label_dimensions_x,
                                       label_dimensions_y))

    plt.axis('off')
    plt.xticks([], [])
    plt.yticks([], [])
    plt.tight_layout()
    plt.xlim(0, label_dimensions_x)
    plt.ylim(0, label_dimensions_y)

    # calc checksum THEN plot barcode
    weight = 1
    chksum = 104
    for x in label:
        chksum = chksum + weight*(ord(x)-32)
        weight = weight + 1
    chksum = chksum % 103
    chkchar = chr(chksum+32)
    label128 = "%s%s%s%s" % ('Ñ', label, chkchar, 'Ó')
    plt.text(label_dimensions_x / 2, label_dimensions_y / 2, label128,
             ha='center', va='bottom',
             fontproperties=prop)
    try:
        plt.savefig(os.path.join("./", label + '.png'))
    except PermissionError:
        logging.warning("Close the current label pdf's before running this script.")

    return

draw_label('123456789')
draw_label('987654321')
draw_label('Test&Show')

Upvotes: 1

Related Questions