[cups] Auto-reformatting shipping labels for a 4x6 thermal printer

Nick Bogdanov nickrbogdanov at gmail.com
Fri Jun 23 22:36:08 PDT 2023


On Mon, Jun 19, 2023 at 12:38 PM Alex Korobkin <korobkin+cups at gmail.com> wrote:
> If you really want to deep-dive into this, one way to handle it is to write
> your own filter.
> Basically, you either register a mime-type conversion
> "application/vnd.cups-nick", and tell your CUPS to convert any incoming job
> into this type, or,you can modify your printer PPD file to include your
> filter, whichever you prefer.
> Once the filter receives the incoming job, you can do whatever you want
> with it: convert, resize, cut, etc. You'd need to do some scripting or
> coding for that, of course.
>
> Check these links for a good explanation of how filters work:
> https://en.opensuse.org/SDB:CUPS_in_a_Nutshell#The_Filter_(includes_the_Driver)
> https://en.opensuse.org/SDB:Using_Your_Own_Filters_to_Print_with_CUPS

I hacked on this for a little bit and came up with something.  It's
not great, but I'm going to try it for a while and see how well it
works for this use case.  Maybe folks on the list have some feedback
on how to improve it.  This is the first time I've ever tried to edit
images programmatically.

Since I wanted to be able to submit labels from multiple CUPS-enabled
computers (Macbooks and Linux PCs), I set up a centralized IPP server
that accepts PDF files and uses a python script to perform the
necessary transformations.  This was as simple as:

ippserver -c ./lconvert.py -f application/pdf -v -p 8100 "Zebra label converter"

lconvert.py accepts a shipping label PDF as input, then for each page
it uses pdfCropMargins to get rid of all the whitespace (which is a
lot if it's on an 8.5x11 page).  It leaves 5px around the edge since
not all of the label material is printable.  It figures out whether
the image needs to be rotated, based on the height/width constants in
the code.  Then it writes out the image as a 203dpi PNG with pixel
dimensions matching what the Zebra printer expects, and sends it to
zplconvert.  Once it has a ZPL-encoded raster image, it gets sent to
the printer over TCP/IP.

I think this could probably work over USB/serial too but I haven't
tested that yet.

I did notice that CUPS on macOS required me to provide a dummy PPD
file in order to send raw PDF data to ippserver.  I copied the PPD
file that Linux CUPS autogenerated for me.  Not sure why the two OSes
behave differently.

#!/usr/bin/env python3

DPI = 203
LABEL_WIDTH_IN = 4
LABEL_HEIGHT_IN = 3
DEFAULT_OUTFILE = "zebra.lan:9100"
#DEFAULT_OUTFILE = "/tmp/out.zpl"

WORKDIR = "work"

# pip3 install --user pdfCropMargins
import pdfCropMargins

# pip3 install pyMuPdf
import fitz

# from `git clone https://github.com/dionysio/zplconvert` (python3 fork)
import zplconvert

import logging, os, socket, sys, tempfile

def sendfile(dest, data):
    (host, port) = dest.split(":")
    ip = socket.gethostbyname(host)
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.connect((ip, int(port)))
        sock.sendall(bytes(data, "utf-8"))

def main():
    logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO)

    try:
        os.mkdir(WORKDIR)
    except:
        pass

    infile = sys.argv[1]
    if len(sys.argv) >= 3:
        outfile = sys.argv[2]
    else:
        outfile = DEFAULT_OUTFILE

    logging.info("Processing PDF %s" % infile)

    (_, cropped) = tempfile.mkstemp(dir=WORKDIR, suffix=".pdf")
    pdfCropMargins.crop(["-p", "0", "-a", "-5", sys.argv[1], "-o", cropped])
    logging.info("Wrote cropped version to %s" % cropped)

    with fitz.open(cropped) as pdf:
        for i in range(0, pdf.page_count):
            page = pdf.load_page(i)
            logging.info(page)

            r = page.bound()
            logging.info("original dimensions: W=%d H=%d" % (r.width, r.height))

            if (r.height > r.width and LABEL_WIDTH_IN > LABEL_HEIGHT_IN):
                page.set_rotation(90)
                x_scale = LABEL_WIDTH_IN * DPI / r.height
                y_scale = LABEL_HEIGHT_IN * DPI / r.width
            else:
                x_scale = LABEL_WIDTH_IN * DPI / r.width
                y_scale = LABEL_HEIGHT_IN * DPI / r.height

            pix = page.get_pixmap(matrix=fitz.Matrix(x_scale, y_scale))
            logging.info("output pixmap: %s" % pix)

            (_, png) = tempfile.mkstemp(dir=WORKDIR, suffix=".png")
            pix.pil_save(png)
            logging.info("wrote PNG to %s" % png)

            converter = zplconvert.ZPLConvert(png)
            converter.set_black_threshold(64)
            converter.set_dither(False)
            zpl = converter.convert(label=True)

            if outfile.find(":") == -1:
                with open(outfile, "w") as out:
                    out.write(zpl)
                logging.info("wrote ZPL data to %s" % outfile)
            else:
                sendfile(outfile, zpl)

if __name__ == '__main__':
    main()


More information about the cups mailing list