Switch printer front-end for SecureDrop Workstation
The current proof-of-concept printing implementation in freedomofpress/securedrop-workstation#277 uses xpp as a printer front-end. We should compare this with other front-end options in terms of:
- usability
- aesthetics
- functionality
and then make a final decision about what front-end to use for the beta.
Screenshots of all dialogs in xpp (taken on Ubuntu 18.04):

Screenshots of all dialogs in gpr (taken on Ubuntu 18.04; cannot show "Configuration" options without configuring a PPD printer)

Screenshots of all dialogs in gtkpl (taken on Ubuntu 18.04)

I have excluded kprinter4, the KDE printing utility available in Ubuntu 18.04, because a) I was not able to get it to actually run, b) the dependencies likely make it prohibitive to install.
Another option that I think is worth considering, however, is to create a very lightweight wrapper around GTK's printing dialog, in combination with the Poppler PDF library or a PostScript library. I was able to use this example code to print PDFs with the GTK dialogs. Here are the GTK dialogs for comparison with the above front-ends, including an example of a preview:
GTK dialogs based on simple Python script using GLib, Gtk, and Poppler

If we used this option, we'd have to "just" find a way to convert every supported file format to PDF prior to printing, but we're already doing some processing to get files to work with lpr.
NB: I would argue that all the above choices, with the possible exception of gpr, satisfy the must-have requirements for a first iteration:
- printing specific pages
- printing multiple copies
- setting print quality
I did not find a print quality option in gpr, but it's possible that this is available in the printer options when a PPD printer is configured. In terms of aesthetics and usability, I think the GTK dialogs are clearly preferable to all other options, using clearer labeling, sensible font and color choices, and making the most common choices (e.g., page range selection) immediately available. It's also most similar to printer dialogs on other operating systems and applications.
We should discuss this soon. The UI of the current xpp is really bad and I could not find any preview option.
Update from 2022-08-11 review with @tina-ux @nathandyer @l3th3 @eloquence @zenmonkeykstop:
- This is still a relevant issue. We are currently using
xppin the closed beta. - Let's discuss further in the context of the overall printer epic.
Still an open issue - we have added xpp to our bookworm apt repo to make it available for 4.2, but it should get replaced.
I got the chance to talk to the lead maintainer of OpenPrinting. I found out that XPP as the front-end in fact does very little. All that it does is surface to the user the printer and document options. CUPS does the remaining processing. However, as we know, some formats CUPS cannot understand. That's why we're already converting LibreOffice files to PDF (which is exactly what the LibreOffice print dialogue does before sending it to cups).
GTK dialogs based on simple Python script using GLib, Gtk, and Poppler
I couldn't find any minimally modern-looking print dialogues, so if this dialogue minimally satisfies our needs, it could be a way to go, if we end up going with the "simply replace XPP approach" (which may only be a temporary solution).
But it is not as complicated as I was originally thinking since it doesn't actually require implementing non-PDF file converters. (CC @zenmonkeykstop).
GTK dialogs based on simple Python script using GLib, Gtk, and Poppler
The need for creating a custom prompt lead me down a rabbit hole. The available alternatives (gpr, gtkpl, etc.) look too outdated and I don't think every application would need to implement its own custom print dialog using whatever UI toolkit they use (GTK, Qt, etc.).
There had to be something else. Something desktop-environment agnostic which applications can just call and the system deals with tit. The approach I found is to use flatpak portals. In reality, these are "XDG"/ freedesktop specifications (thus not directly tied to flatpak) - they are just GBus calls. And I don't think we have to install anything extra. It should be installed by default.
For example, with the following line we can open a dialog:
gdbus call --session --dest org.freedesktop.portal.Desktop --object-path=/org/freedesktop/portal/desktop --method org.freedesktop.portal.Print.PreparePrint "program-name" "title" [] [] []
The image on the left is fedora and the image on the right is Debian. As you can see, there are slight differences, but I'm guessing this is because they are using different versions of xdg-desktop-portal-gtk.
We can also pass in parameters and a file descriptor, but unfortunately DBus is not very well documented from my experience. I've got some luck learning about how to use it by running dbus-monitor --session and then running the Evince (PDF viewer) flatpak and seeing how it call a print dialog. If we go down this router, the next steps would be to find out how to pass in the file descriptor properly, but that should be about it.
After the initial print dialog call, we then need to issue a second call for actually printing. I haven't exactly understood how to make this call, but inspecting the DBus "traffic" from Evince, it looks something like this:
Comparison with alternatives
Advantages:
- no need for custom code (technically -- doing the proper calls / parsing may actually end up requiring some code :/ )
- uses default system's default front-end (will look as modern as the default system UI Toolkit installed on the system)
- This option checks all the above-listed requirements
Disadvantages:
- added development complexity due to the need to parse GDBus messages. There are some ways to do this in Python, but they require python libraries and some are even specific to UI Frameworks (like the GTK one -- see here an implementation example)
References:
- DBus Print portal documentation (and its respective low-level API
- DBus debugging tools
Qt Dialog
I've also looked into what the default Qt print dialog would look like.
| Default view | "Options >>" expanded |
|---|---|
| Clicking on "Properties" button | Double-sided Options tab |
Regarding requirements:
- :heavy_check_mark: printing specific pages
- :heavy_check_mark: printing multiple copies
- :x: setting print quality
:information_source: update: the Qt dialog cannot handle when the printer is disconnected and reconnected while the dialog is up, which could be helpful for faster troubleshooting. Furthermore, the GTK dialog does show status information (e.g. connecting to printer...) beside the printer.
boilerplate Qt5 python code used
First you have to install PyQt5. Assuming you're on Debian, you need to run sudo apt install python3-pyqt5.
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QPushButton, QLabel
from PyQt5.QtGui import QPainter
from PyQt5.QtPrintSupport import QPrintDialog, QPrinter
from PyQt5.QtCore import Qt
class HelloWorldPrintDialogWidget(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle("Your Options Tab Title")
layout = QVBoxLayout()
self.label = QLabel("Print more text?")
layout.addWidget(self.label)
self.print_more_text_button = QPushButton("Print More Text")
self.print_more_text_button.setCheckable(True)
self.print_more_text_button.toggled.connect(self.setPrintMoreText)
layout.addWidget(self.print_more_text_button)
self.setLayout(layout)
self.print_more_text = False
def setPrintMoreText(self, state):
self.print_more_text = state
def printMoreText(self):
return self.print_more_text
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle("Printing Tutorial")
self.setGeometry(100, 100, 400, 300)
self.optionsWidget = HelloWorldPrintDialogWidget()
print_button = QPushButton("Print Hello World", self)
print_button.clicked.connect(self.printDialog)
self.setCentralWidget(print_button)
def printDialog(self):
printer = QPrinter()
printer.setFullPage(True)
print_dialog = QPrintDialog(printer, self)
print_dialog.setWindowTitle("Print Hello World")
print_dialog.exec_()
if __name__ == '__main__':
app = QApplication(sys.argv)
mainWin = MainWindow()
mainWin.show()
sys.exit(app.exec_())
GTK dialogs based on simple Python script using GLib, Gtk, and Poppler
After much searching, I finally found something which creates a GTK dialog without the need for any code of ours. The program is yad (yet another dialog). It's a dialog tool that unlike zenity, actually has print dialog options :raised_hands:. With a simple call like this:
yad --print --filename ~/QubesIncoming/sd-dev/building-qubes.pdf
We get the standard GTK prompt, without the need for digging into DBus or create our own custom GTK print dialog.
Furthermore, installing it in sd-bookworm-large-template should only add 173KB because we already have installed the GTK and other dependencies. I don't know what others think but in my eyes, this looks like the most suitable XPP replacement candidate. It should satisfy all requirements (after all, it's a default GTK dialog)
Update: I'm having basic issues with yad. When printing PDF files it is literally just printing the raw file (literally printing the start of the file as if it were a binary text file) and I simply cannot print image files (yad:9966): Pango-WARNING **: 15:25:53.310: Invalid UTF-8 string passed to pango_layout_set_text(). For this reason alone, we should discard this print dialog.
Hey @deeplow, thank you so much for looking into all these options. (And for the sample qt code).
It looks like yad has had recent releases/maintenance, but one maintainer at the moment based on github activity, and some potential limitations (no signed releases/commits, no CI, etc).
I think feel more confident with a printer frontend that was already upstreamed into Debian and/or had a little more of a process around it re code review and release, or (I hate to say it) maybe our own tiny qt package if that's really all it takes is the snippet above. What are your thoughts?
FYI, it is in fact in Debian.
We could go the Qt way as well. I would argue it doesn't look as polished compared to the GTK one, but it gets the job done (except for the print quality).
Cool, thanks for letting me know. It's not exact, but I'm looking for the balance of usability/ease of inclusion vs rough dependency addition heuristics (even in an untrusted vm). With the limited activity, no security policy/ci/code-signing/etc in the repo, if there are no other suitable alternatives, I'm personally leaning towards the Qt or GTK way, whatever is easiest, because they are all an improvement over the current, and because ideally this will be the state only until we (eventually?) rework the print workflow to print from viewer, where we'll be able to use the print dialogs of whatever application is opening the file. But open to others' thoughts.
That's true. This is a temporary solution. So as long as it is an improvement and satisfies the requirements, we should be fine.
I just came across a limitation of the Qt dialog compared to the GTK one:
ℹ️ update: the Qt dialog cannot handle when the printer is disconnected and reconnected while the dialog is up, which could be helpful for faster troubleshooting. Furthermore, the GTK dialog does show status information (e.g. connecting to printer...) beside the printer.
Depending on how we go about handling driverless printining, printer status information and printer status could be delegated to the printer itself instead of the client needing a back-and-forth to communicate this information to the user. CC @nathandyer, I can imagine this being helpful. Don't know if you have oppinions on the GTK / Qt dialog options.
Thanks @deeplow! Although I'm generally a fan of the GTK way of doing things, for this purpose and in this environment, I don't have any strong feelings in preferring one toolkit over another. I agree that showing status information next to the printer, and being able to handle disconnects more elegantly, makes the GTK dialog a stronger option. I also think the layout is a little more straightforward (fewer collapsable panels and additional windows/dialog boxes for viewing properties and setting print options).
That being said, I don't think those benefits are make-or-break, and if Qt is easier to implement or works better in our existing Qt-based world, it probably makes sense to go with that option instead.
Thanks for taking a look. Sadly I spoke too soon about the Qt implementation. For some reason I forgot to test if it actually printed correctly. It turns out that I was just setting up the dialog and the print actually does nothing. It's just a blank dialog. So we also need document rendering-code. I have found this example which does that, but it was something I was kind of avoiding because CUPS can already handle printing.
I would have expected for there to be a way to have Qt / GTK dialogs using the CUPS back-end and not having to make a rendered for any kind of file we print. This is disappointing and the documentation it not being very helpful. There may be a way to do it, but I am not finding any reference to it.
Supposedly GTK already has the CUPS Common Printing Dialog Backend in version 4.0 (which I don't know how well it runs on Qubes). But I have seen no docs on how to take advantage of it in python at least. I am guessing this should be automatic and not really a concern for us. On the Qt front from what I can see, it still hasn't been merged.
After deciding to go the GTK print dialog route, I investigated the best way to implement it (turns out there are many print dialogs in GTK) and the whole ecosystem is a bit of a mess regarding printing. Here are some of the my notes which I had forgot to post.
Goal
Implement a GTK file dialog without implementing document rendering (that should be handled by CUPS via cupsfilters).
Relevant links:
- History of GTK print dialogs.
- C GTK 4 docs
- Python GTK 4 docs - Unofficial, but actually usable
GTK.PrintOperation
According to the GTK3 docs:
Then you start the print operation by calling gtk_print_operation_run(). It will then show a dialog, let the user select a printer and options. When the user finished the dialog various signals will be emitted on the GtkPrintOperation, the main one being GtkPrintOperation::draw-page, which you are supposed to catch and render the page on the provided GtkPrintContext using Cairo.
We probably want to avoid reinventing the wheel and not render all kind of files. CUPS already knows how the handle most and the ones which it can't handle (LibreOffice), we are already handling the conversion.
GTK.PrintDialog
GTK 4.14 (available only in Fedora 40+ and likely only on Debian Trixie) introduces a much more sane printing API: the PrintDialog. It was introduced in this merge request and it includes a sample printing application demo here.
Boilerplate C code (example)
static void
print_hello (GtkWidget *widget,
gpointer data)
{
g_print ("Hello World\n");
}
static void
activate (GtkApplication *app,
gpointer user_data)
{
GtkWidget *window;
GtkWidget *button;
GtkWidget *box;
window = gtk_application_window_new (app);
gtk_window_set_title (GTK_WINDOW (window), "Window");
gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_halign (box, GTK_ALIGN_CENTER);
gtk_widget_set_valign (box, GTK_ALIGN_CENTER);
gtk_window_set_child (GTK_WINDOW (window), box);
button = gtk_button_new_with_label ("Hello World");
do_pickers(window);
g_signal_connect (button, "clicked", G_CALLBACK (do_pickers), NULL);
g_signal_connect_swapped (button, "clicked", G_CALLBACK (gtk_window_destroy), window);
gtk_box_append (GTK_BOX (box), button);
DEFAULT
gtk_window_present (GTK_WINDOW (window));
}
int
main (int argc,
char **argv)
{
GtkApplication *app;
int status;
app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
status = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return status;
}
The above code can be compiled with gcc $(pkg-config --cflags gtk4) pickers.c $(pkg-config --libs gtk4)
The resulting application will let you choose files, then print them. I needed to comment some check to see if it was a PDF file. But it also prints other file types (probably the ones supported by CUPS filters).
Gtk.PrintUnixDialog (GTK)
This is rarely used in practice but both Chrome and Firefox do make use of it: print_dialog_gtk.cc print_dialog_gtk.cc (2)
According to the docs:
GtkPrintUnixDialog implements a print dialog for platforms which don’t provide a native print dialog, like Unix.
// Getting the settings
print_dialog_ = ui::LinuxUi::instance()->CreatePrintDialog(this);
dialog_ = gtk_print_unix_dialog_new(nullptr, nullptr);
if (gtk::GtkCheckVersion(4)) {
gtk_window_set_hide_on_close(GTK_WINDOW(dialog_.get()), true);
} else {
g_signal_connect(dialog_, "delete-event",
G_CALLBACK(gtk_widget_hide_on_delete), nullptr);
}
// We don't really need to load saved settings since sd-devices is disposable
// print_dialog_->LoadPrintSettings(*settings_);
// GetLastUsedSettings().SetLastUsedSettings(gtk_settings_);
// Response will get deprecated GTK 4.10 https://docs.gtk.org/gtk4/class.PrintUnixDialog.html
// However, the full deprectation will probably take until GTK 5 https://discourse.gnome.org/t/14194
signal_ = ScopedGSignal(
dialog_, "response",
base::BindRepeating(&PrintDialogGtk::OnResponse, base::Unretained(this)));
gtk_widget_show(dialog_);
gtk::GtkUi::GetPlatform()->ShowGtkWindow(GTK_WINDOW(dialog_.get()));
GtkPrintJob* print_job =
gtk_print_job_new(base::UTF16ToUTF8(document_name).c_str(), printer_,
gtk_settings_, page_setup_);
gtk_print_job_set_source_file(print_job, path_to_pdf_.value().c_str(),
nullptr);
gtk_print_job_send(print_job, OnJobCompletedThunk, this, nullptr);
When running the dialog I see an error message:
** (python3:3967): WARNING **: 10:27:14.837: failed to contact colord: Error calling StartServiceByName for org.freedesktop.ColorManager: Unit colord.service is masked.
Turns out colord.service is disabled by defauly on Qubes Systems via a systemd preset. In the commit message there is no stated reason for the removal other than it not being necessary.
I have temporarily unmasked the service with sudo systemctl unmask colord. But I think this is not super needed.
Overall this seemed to work well for our use-case:
- available on Debian Bookworm (12)
- Does not require document rendering
But it does have one issue we should resolve
BUG: page range not working
Selecting a certain page range was not working. I tried identifying if the issue is with CUPS not dealing well with it or if it's somehow GTK not passing the file along.
To debug, I enabled debugging in CUPS with sudo cupsctl --debug-logging and then sudo systemctl restart cups.
The debug log shows at /var/log/cups/error_log. It was a lot of output, so I grepped for argv (the parameter with which pdftops was being called). I saw the following
D [13/Dec/2024:14:19:11 +0000] [Job 28] argv[5]="number-up-layout=btlr number-up=1 Resolution=300dpi PageSize=A4 noCollate job-uuid=urn:uuid:124e6820-7d52-3387-57ca-721d7ef1215f job-originating-host-name=localhost date-time-at-creation= date-time-at-processing= time-at-creation=1734099551 time-at-processing=1734099551"
As you can see, the page range is not passed to pdftops. This means that it's either lost by CUPS somewhere before or it's not passed at all to CUPS.
When calling the same printer via lp -d "Generic-CUPS-PDF-Printer" -P 3 file.pdf we indeed get the correct page-range to
be passed along.