2

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.

Sam
  • 113
  • 1
  • 1
  • 5
  • Since you're using a font, is there a reason you're converting the barcode to an image and embedding the image, rather than embedding the text into the PDF directly? – Mark Ransom Oct 25 '18 at 04:04
  • Not really. I don't see how I'm converting the barcode to an image first, I'm writing directly using plt.text and setting the fontproperties to the barcode properties, right? Am I not directly embedding the text that way? – Sam Oct 30 '18 at 10:16

2 Answers2

1

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')

Brian Anderson
  • 1,661
  • 1
  • 17
  • 27
  • Unfortunately, because I am writing this (simplified) piece of code for existing applications, I cannot convert to a different code type. The start and stop characters I thought I checked, and I thought were absent on the barcode labels I need to replace. Will double-check that. Substituting a text font in my barcode does not help, it only shows that only the barcode is not rendered correctly, though the rest of the text is. – Sam Oct 24 '18 at 13:09
  • It is thus not as if the barcode is clipped of off the 'canvas', see post edit. – Sam Oct 24 '18 at 13:21
  • Have you tried moving the 'plt.show()' to the end after 'plt.savefig(...)'? – Brian Anderson Oct 24 '18 at 21:15
  • Thanks! For me the .png solution works, too. Converting the .png to .pdf shouldn't be too hard. Thanks for taking the effort of including the checksum and start/stop characters, too. – Sam Oct 25 '18 at 07:20
  • have tried that. Doesn't work unfortunately. Also tried using plt.draw() instead of plt.show(). – Sam Oct 25 '18 at 07:21
  • One thing that still shows weirdly is the display of special characters such as ñ or ó. It displays as a , also in the barcode. Do you know of any free code128 fonts that are capable of displaying these characters? – Sam Oct 25 '18 at 08:30
  • The Ñ' and 'Ó' characters (should be capitalized, by the way) are the start and stop characters in the font I am using: "https://github.com/Holger-Will/code-128-font". We may have different fonts. In which case the font you are using should have a conversion table that lists the high ASCII equivalents of the control characters needed to trigger the font to display Code128B Start (104) and Code 128 Stop (106). – Brian Anderson Oct 25 '18 at 11:36
  • @Sam you need to worry about more than just the start and stop characters, or you'll run into some barcodes that just don't work because you can't print their checksum properly. – Mark Ransom Oct 25 '18 at 13:39
  • @Mark Unless I have the mapping table from (checksum) value to the corresponding character, right? And you have that in the form of ASCII chr conversion? – Sam Oct 25 '18 at 14:36
  • @Brian, I downloaded your font such that it displays the start/stop char's correctly. The checksum value for, for example, 'AM-H-10-01-1' is 48, translating into an ASCII character 'P'. Including this checksum character in my label, however, still doesn't make the label scannable. Or this I miss something here? – Sam Oct 25 '18 at 14:40
  • 1
    @Sam you must have missed something, because both my code and Brian's return `'<'` for that string. – Mark Ransom Oct 25 '18 at 15:11
  • @MarkRansom Right you are, a 28 checksum value results in an ASCII character symbol for the less-than sign ('<'). That's 28 + 32 (to convert to Code 128) or decimal 60. By the way, the Holger-Will font isn't mine, but it is GPL3. I normally don't use fonts. – Brian Anderson Oct 25 '18 at 15:40
  • 1
    I don't normally use barcode fonts either, I'm quite comfortable with generating my own images. It's an interesting hack to use matplotlib just for its ability to output PDF. – Mark Ransom Oct 25 '18 at 16:12
0

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')
Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • The reason I did not use the PIL library is because I want to generate text besides of the barcode. I felt as if the matplotlib library would be more suitable for "drawing" as if drawing on a canvas. First, I'd like to generate this kind of canvas, to afterwards fill it in with barcodes and accompanying text, etc. Or is this possible with PIL, too? – Sam Oct 30 '18 at 14:52
  • @Sam obviously I don't know all that you're trying to do, so I can't say anything useful about the other parts of your problem. I know that PIL can put text in an image but that's not the same thing, as you won't be able to select the text in the PDF for example. I would be looking at something more document oriented based on your comment, but I don't know what that would be. – Mark Ransom Oct 30 '18 at 15:30