Skip to main content

Quite often, there is a need to automate a specific process. In this case, a client had a manual process in place where people printed specific type of documents at certain periods. There was some space for human error, people forgetting to print something, people not being able to print everything on time, people printing the same documents twice, etc. The human task of people manually printing documents can be automated on the application level, by creating a scheduled task for printing documents on a network printer. In order to achieve that, we came up with this…

The solution is a containerized CUPS server with appropriate drivers and printer configuration. We had to create a new Docker image with CUPS, which will serve as the CUPS server, then get the correct drives for the printer (since we are going to use a network printer and make the appropriate printer configuration). Let’s get to know more about CUPS before we go into the actual implementation.

What is CUPS?

CUPS is a modular printing system for Unix-like computer operating systems which allows a computer to act as a print server. A computer running CUPS is a host which can accept print jobs from client computers, process them, and send them to the appropriate printer. CUPS uses the Internet Printing Protocol (IPP) as the basis for managing print jobs and queues. CUPS is free software, provided under the Apache License.

How does it work?

The initial step requires a queue that keeps track of printers status. When you print to a printer, CUPS creates a queue for tracking the printer status and any pages you have printed. A queue can point to a local USB port connected printer, but it can also be a network printer or maybe even many printers on the internet. Where the printer resides doesn’t matter, the queue is independent of this fact and looks the same in any given printer environment.

Every time you print something, CUPS creates a print job which is consisted of the destination queue where documents are sent to, name of those documents, and its page descriptions. Job is numbered queue-1, queue-2, etc. so you can track the job as it is printed or cancel. CUPS is deterministic. When CUPS gets a job for printing, it determines the best programs filters, printer drivers, port monitors, and backends to convert the pages into a printable format and then runs them to actually print the job. After the print job is completed, the job is removed from the queue and then CUPS moves on to the next one. Notifications are also available when the job is finished or some errors occurred during printing there are multiple ways to get a notification on the outcome.

Ready, steady, Docker run

Let’s containerize first. The initial step is to set the base docker image. For this Dockerfile, we have decided that we are going with CentOS Linux distribution, by RHEL since it provides the cups packages from the regular repository. Other distributions might require premium repositories in order for cups packages to be available. The entry instruction which specified the OS architecture:

FROM centos:8

The next and more important step is, installing the packages: cups and cups-filters. The first one, cups, provides support for the actual printing system backend, filters and other software, whereas cups-filter is a required package for using printer drivers. With the dandified yum we update and install necessary dependencies:

RUN dnf update -y && \
	dnf install -y cups cups-filters openssl

# Install OpenJDK java 11
RUN dnf install -y java-11-openjdk && \
	dnf clean all && \
    rm -rf /var/cache/dnf

RUN java -version

ENV JAVA_HOME="/usr/lib/jvm/jre" \
    JAVA_VENDOR="openjdk" \
    JAVA_VERSION="11.0"

With that, JDK is available and we can confirm by running java –version.

Next follows the configuration for the cups server. This is done in the file named cupsd.conf, which resides in the /etc/cups directory of the image. A good practice here would be to create a copy of the original file.  In the cupsd.conf file each line can be configuration directive, blank line or a comment. Directive name and values are case insensitive, comments start with a # character.

The patching we did, on top-level directive DefaultEncryption set the value of IfRequested, to only enable encryption if it is requested. The other directive, Listen, add value 0.0.0.0:631 in order to allow all incoming connections.

RUN sed -e '0,/^</s//DefaultEncryption IfRequested\n&/' -i /etc/cups/cupsd.conf
RUN sed -i 's/Listen.*/Listen 0.0.0.0:631/g' /etc/cups/cupsd.conf

Allow the cups service to be reachable:

RUN /usr/sbin/cupsd \
  && while [ ! -f /var/run/cups/cupsd.pid ]; do sleep 1; done \
  && cupsctl --remote-admin --remote-any --share-printers \
  && kill $(cat /var/run/cups/cupsd.pid)

After the service setup is done, the network printer and its drivers’ configuration follows. In our scenario, we used Ricoh C5500 printer. A good resource for finding appropriate driver files for the printers would be: https://www.openprinting.org/

COPY conf/printers.conf /etc/cups/printers.conf
COPY conf/ricoh-c5500-postscript.ppd /etc/cups/ppd/ricoh-printer.ppd
COPY examples/accident-report.pdf /tmp/accident-report.pdf

A bit more general info on printer drivers: PostScript printer driver consists of a PostScript Printer Description (PPD) file that describes the features and capabilities of the device, then, filter programs that prepare print data for the device, and support files for colour management. Not only that but also, links with online help, etc. These PPD files include references to all of the filters and support files used by the driver, meaning there are details on all features that are provided by the driver. Every time a user prints something the scheduler program, cupsd service first, determine the format of the print job and the programs required to convert that job into something the printer can understand and perform. CUPS also includes filter programs for many common formats, for example, to convert PDF files into device-dependent/independent PostScript. All printer-specific configuration such is an IP address of the printer should be done in the printers.conf file.

Last but not least, we need to start the CUPS service:

CMD ["/usr/sbin/cupsd", "-f"]

Now everything is in place on the docker side. But then, somehow the print job needs to be triggered. That brings us to the final step, creating a client from the application mid-layer, which needs to set off a print job and the CUPS server will take care of the rest.

CUPS4J

For our solution, we used cups4j, a java library which is available in the mvn central repository. Basic usage of cups4j requires:

  • Setting up a CupsClient
  • Fetching an actual file
  • Creating a print job for that file
  • Printing (triggers the print job)

We also implemented a scheduler which will trigger this job weekly, meaning all documents will be run in a print queue once a week. If we want to specify custom host, then we need to provide the IP address of that host and the appropriate port number.

CupsClient cupsClient = new CupsClient("127.0.0.1", 631);
CupsPrinter cupsPrinter = cupsClient.getDefaultPrinter();
InputStream inputStream = new FileInputStream("test-file.pdf");
PrintJob printJob = new PrintJob.Builder(inputStream).build();
PrintRequestResult printRequestResult = cupsPrinter.print(printJob);

Summary

We managed to create dockerized solution step by step. First, we created an image that runs CUPS server, which we configured to a specific network printer. Then the printer waits for a print job to be triggered by the client. As a client, we created a cups4j simple client which raises the print job. Meaning all CUPS related configuration is done in Docker and the client only triggers the print job.