Watermarking printouts in the Linux world

Printouts most often are a waste of paper.

But much worse is obsolete stationary. It’s not only as a waste of paper, but a waste of money, too, as companies typically have it printed for money.

With today’s high quality color laser printers in many offices, a better solution can be created by watermarking your printouts accordingly. But while many Windows printer drivers support it right out of the box, Linux and its “Common Unix Printer Subsystem” (CUPS) is surprisingly unprepared for this. Fortunately, as it is Linux, you can create your own solution rather easily.

A really short abstract on CUPS printing

I won’t be going into details here, because learning CUPS is worth an article series all by itself. But to cut things short, it’s sufficient to keep the following facts in mind:

  • When creating your printing infrastructure with Linux, you’re handling Postscript most of the time. Or rather, CUPS is doing so.
  • You can easily create a print server on any Linux box and have all your client stations learn the available printer queues from that box (and you can even have “MS Windows” clients use those queues, too).
  • Clients send their print jobs to their local CUPS instance, which forwards the jobs to the selected printer queue on the print server.
  • A “filter” for that printer queue will handle the conversion from Postscript to the printer-specific format.

Again, there’s no out-of-the-box watermarking component in this picture. But with a few tools (typically available for your Linux distro, too) you can add this functionality yourself.

The goal

We wanted to have various printer queues, one for each combination of printer and type of stationary, with the print server doing all the work – especially mixing in the stationary layout.

By selecting the proper queue, the user would have their print job “watermarked” with what makes up that specific form or letter paper. The printers only need a single paper bin and feed on plain, white paper. Of course, if you require different paper sizes or weight, you’ll still have to have those multiple sources.

The solution

To achieve this, we had to do the following:

  1. Create a PDF that contains the stationary’s design. This was no extra work, as this is already the output of the original process and would be needed for separate printing, too.
  2. Create a tool chain to merge the “watermark” PDF and the actual print job.
  3. Integrate the tool chain into the CUPS processing.

We selected “pdftk” as our central tool to add the watermark to the print job. And since CUPS is using PostScript, which is the same “language” that PDFs use, all we had to do is convert the print job to a PDF, use “pdftk” to watermark, and convert the resulting PDF back to Postscript. All this packed can be packed into a shell script like the following:


tempdir=$(mktemp -d)

echo $(date) "$0 $* (tempdir: $tempdir)" >> $logfile

# Command line arguments

if [ -z "$filename" ] ; then

if [ $# -ge 7 ]; then
cat $6 > $tempdir/ps.in
cat > $tempdir/ps.in

# convert Postscript to PDF
/usr/bin/ps2pdf $tempdir/ps.in $tempdir/pdf.in 2>>$tempdir/err

# watermarking
/usr/bin/pdftk $tempdir/pdf.in background "$watermark" output $tempdir/pdf.out 2>>$tempdir/err

# convert PDF to Postscript
/usr/bin/pdftops $tempdir/pdf.out - 2>>$tempdir/err

# clean-up
rm -rf $tempdir

“pdftk” wouldn’t read stdin, so you have to take a slight detour – which helps during debugging, too. And of course there’s plenty that could be improved… starting with a way to specify the watermark PDF via some option, rather than hard-coding it into the script. Also note the seemingly unnecessary “filename” variable – it’s just there to illustrate what to look out for when reworking this sample script.

You may have noticed that while we use “ps2pdf” to convert the input stream, we don’t use “pdf2ps” to convert back. We experienced significant print quality problems and color changes with “pdf2ps”, while “pdftops” handled the job quite nicely. And looking at Stefaan Lippen’s blog page on the subject, there may be other severe drawbacks when using pdf2ps, too. Especially the significant increase in output file size may i.e. impact memory consumption and network traffic for networked Postscript printers.

Place that script in CUPS’ filter directory, which is “/usr/lib64/cups/filter/” in our case (obviously, a server running a 64bit variant of Linux – SLES11 SP1).

Now how to invoke this script? When creating your queue, CUPS creates a PPD (“Postscript Printer Definition”) file from the printer-specific PPD and saves it under /etc/cups/ppd/<queuename>.ppd. All you have to do is add a single line to that file, somewhere in the head section:

*cupsFilter: “application/vnd.cups-postscript 100 watermarkpdf”

The documentation of that command can be found on the CUPS web site. “watermarkpdf” was the name I gave to the script when testing this, so you may need to replace this with your own script’s name.

With a script per (watermarked) queue, you’re ready to rumble. Oh, and if anyone has an easy way of including the watermark file name in individual print jobs or at least in the queue’s PPD, please drop me a note in the comments section. Thanks!

This entry was posted in CUPS, howto, Linux and tagged , , . Bookmark the permalink.

11 Responses to Watermarking printouts in the Linux world

  1. vincent says:

    Maybe I am the first one to use this 🙂
    When I copy the script from this webpage, the “quotes” are mostly wrong, so not straight quotes, but ‘fancy’ quotes like “
    My two cents, i’ll let you know how thing work out after reworking the script.
    Thanks a bundle for pointing me to the right solution!

    • jmozdzen says:

      Hello Vincent,

      thank you for your comments and for pointing out the trouble when copying the source. I had inserted the script source via cut&paste, seems like the editor or WP converted some of the characters – but not everywhere. In addition to the double quotes, a dash was rendered useless, too.

      The updated script is now uploaded to the blog and referenced in the article, I’ll see what I can do about the inline source, too.

      With the characters back in order, have you been successful in running the script?


  2. vincent says:

    Hello Jens,
    thank you for the find os the dash! I seemed to work with the ‘wrong’ dash as well.

    I am reworking the script a little for my own needs (I will use ‘stamp’ not ‘backgroud’ for the pdftk since i want to do ‘real’ watermarking).

    I also added a trap command for removing the temp folder.

    Last but not least I am looking into your question on how to pass something to the script. My best guess is to send
    lp -d MyPrinter -o FilterOption=”here is the new sentence”

    which should show op in options=”$5″ and filter it out there.
    then to craft it into a custom ps file, but you could for instance also give the filename of another stationary there.

    I’ll be in touch when I finish

  3. Alexandre Magno says:

    A problem. In my PPD:

    *cupsFilter: "application/vnd.cups-raster 0 /opt/epson-inkjet-printer-201108w/cups/lib/filter/epson_inkjet_printer_filter"
    *cupsFilter: "application/vnd.cups-postscript 100 /usr/lib/cups/filter/bg-mxcp-pdf"

    Then bg-mxcp-pdf no executes because of priority. And if I invert the priority, it prints TeX codes.

    • j mozdzen says:

      Hi Alexandre,

      it’s by design that you can have only a single filter per job (see i.e. http://osdir.com/ml/printing.cups.devel/2004-10/msg00026.html)

      If you need to chain those two filters, you might work around the problem by simply calling the second filter script from within your first filter script… as it are filters, just make sure the output from your last processing step is piped into the second filter script.


      • Gait says:

        You can have more than one filter, but not thru a ppd-file.

        Make a script which at least acts as a simple cat(1). In /etc/cups/mime.convs (Re: mime.convs(5)), replace the line

        application/postscript application/vnd.cups-postscript 66 pstops

        with the next two lines:

        application/postscript application/letterhead 66 letterhead2ps
        application/letterhead application/vnd.cups-postscript 66 pstops

        so the script letterhead2ps always sits in the pipeline of filters, just before pstops is called.

        You also need to put


        as a new (intermediate) type into


        Re: mime.types(5)

        I made such a filter, which interprets its args and converts input to pdf, uses pdftk to combine it with a pdf-file containing the letterhead or a logo or other artwork, and converts this into PostScript.
        If there no args she knows of, she just acts as a cat.

        One very important thing is the 66, which is less than 100: it’s about virtual costs and so this line is preferred to the standard line which coasts 100 to make application/vnd.cups-postscript

        I also made use of CUPS’s so called printer instances:

        Dest netlp/printedlogo PageSize=A4 Duplex=DuplexNoTumble printedlogo

        If the script, when used as a filter, sees ‘printedlogo’ she makes PostScript including a logo.

        This is what I called postprinted letterhead.

  4. Alexandre Magno says:

    My ask (in portuguese): http://goo.gl/XQHn94
    I might be rewriting it in english.

  5. Alexandre Magno says:

    My “question about CUPS filter” is in english now: http://goo.gl/XQHn94

  6. Alexandre Magno says:

    Looks like I need to consider that PDF is the standard print job format. But I don’t know do it.

  7. Alexandre Magno says:

    /usr/lib/cups/filter/pdftopdf "$1" "$2" "$3" "$4" "$5" $tempdir/cups-pdf.out 2>>$tempdir/err
    /usr/lib/cups/filter/pdftoraster "$1" "$2" "$3" "$4" "$5" $tempdir/raster.out 2>>$tempdir/err
    /opt/epson-inkjet-printer-201108w/cups/lib/filter/epson_inkjet_printer_filter "$1" "$2" "$3" "$4" "$5" >$tempdir/err

    cat $tempdir/err:

    DEBUG: mediaBox = [ 0.000000 0.000000 420.000000 596.000000 ]
    DEBUG: size = A5
    Can't open PPD file.

  8. Alexandre Magno says:

    http://paste.ubuntu.com/6073711/ exits with code 1, but $tempdir/raster.out is generated

Leave a Reply