How to Write a Printer Driver

Body

A printer driver in BeOS R5 is an add-on that exports a specific C interface. This add-on is used by the print_server to add a new printer, configure a page, configure the print job, and print the print job on the printer. This article describes how the print_server interfaces with the printer driver.

The print_server is responsible for maintaining common settings. BMessage objects are used to pass settings from the print_server to a printer driver and vice versa. If the printer driver has to return a BMessage object and wants to show the successful completion of an operation, it has to set the field what to the value 'okok'. To indicate an error, the preferred method is to return NULL. In some cases the print_server interprets a value in what other than 'okok' as the failure of an operation. A printer driver should not rely on this; instead, it should return NULL.

A print job is a file, generated by a printing application using the Interface Kit class BPrintJob (see Figure 1). This file contains archived BPicture objects — one for each call made to BPrintJob::DrawView(). A raster printer driver, for example, can use the BPicture objects for each page and draw them into a BBitmap object and convert the pixels in the BBitmap object into the format the printer understands. The driver can make changes to the content of the BPicture, including dithering and color correction. There is no standard or common code for these features in R5.

When a printer driver is requested to print a print job it uses a transport add-on to transfer data to the printer. Transport add-ons know how to write to an individual device (parallel, USB, network, etc.) Separating transports from drivers allows R5 to work with, for example, a printer on a USB port or the same printer on a parallel port with only one driver. It also makes writing the drivers simpler: the driver outputs a "stream" of data, without having to know how it is delivered.

Figure 1: BeOS R5 Printing Overview

Driver Location

System printer drivers that are installed with the OS are located in B_BEOS_ADDONS_DIRECTORY in the subfolder Print (i.e., /boot/beos/system/add-ons/Print).
Printer drivers installed by the user are usually placed into B_USER_ADDONS_DIRECTORY in the subfolder Print (i.e., /boot/home/config/add-ons/Print).

Transport add-ons (like Print To File, Parallel Port, and USB Port) are located in a folder named transport inside of the system or user printer driver folder.

The Life of a Printer Driver

Installing the Printer Driver

To install a printer driver, the driver has to be moved into one of the printer driver folders. Printer drivers should not link to shared libraries other than those provided by the OS — third-party libraries should be statically linked instead. This helps to avoid version conflicts.

Adding a New Printer

Users add a new printer to the system using the Printers preflet. They select a printer name, printer type, and transport add-on. The preference application creates a spool folder with the name of the printer in B_USER_PRINTERS_DIRECTORY (i.e., /boot/home/config/settings/printers). The name of the transport add-on is stored in the spool folder's file attribute named transport. The type code of this attribute is B_STRING_TYPE.

The print_server, when notified of the new printer, calls the printer driver to configure the printer. The function prototype within the printer driver for this is:

char* add_printer(char* printer_name);

This gives the printer driver the chance to open a window for configuration of the printer model. The configuration can be stored in the attributes of the spool folder. On success the printer driver should return the pointer to the string printer_name or NULL on failure.

Configuring the Page

When an application calls BPrintJob::ConfigPage(), the print_server requests that the printer driver configure the page by calling:

BMessage* config_page(BNode* spool_folder, BMessage* settings);

The printer server calls this function with spool_folder, a pointer to a BNode object whose path is the spool folder, so the printer driver can access its attributes. It also passes settings, a pointer to a BMessage object that contains the previous page settings, if any. At the very least, the page size and orientation have to be specified.

The printer driver usually opens a window to let the user select the page size, orientation, and (optionally) other settings; these other settings may be printer specific. The mandatory fields are:

Field Type Code Meaning
printable_rect B_RECT_TYPE The printable rectangle in 1/72 inches. That is the area the printer is able to write into.
paper_rect B_RECT_TYPE The paper rectangle in 1/72 inches.
orientation B_INT32_TYPE 0 ... portrait, 1 ... landscape.
xres B_INT32_TYPE Horizontal DPI.
yres B_INT32_TYPE Vertical DPI.

If the configuration was successful, a new BMessage object with the settings is returned. If it fails, NULL has to be returned (e.g. in response to the user clicking the Cancel button in the page setup window).

Configuring the Print Job

The main purpose of the configuration of the print job is to let the user select the range of pages to be printed and the number of copies of each page that should be printed. Again, printer-driver-specific settings can be added.

The print_server calls this function with the same parameters as it does when configuring the page:

BMessage* config_job(BNode* spool_folder, BMessage* settings);

Settings contains the fields from page configuration, which should not be changed. The mandatory fields that have to be added or changed are:

Field Type Code Meaning
first_page B_INT32_TYPE The page number of the first page to be printed. Starts with 1.
last_page B_INT32_TYPE The page number of the last page to be printed. If all pages should be printed its value should be MAX_INT32 (= 0x7fffffffL).
copies B_INT32_TYPE The number of copies.

On success, a new BMessage object with the settings should be returned; otherwise NULL has to be returned.

Using a Transport Add-On

The printer driver has to load the transport add-on associated with the printer. The name of the transport add-on is stored in the file attribute transport of the spool folder. The driver should first look into the user's printer folder (start at B_USER_ADDONS_DIRECTORY then proceed to Print/transport) to find the named transport add-on there. If it fails, it should look into the system printer folder (start at B_BEOS_ADDONS_DIRECTORY then proceed to Print/transport).

The transport add-on exports two C functions:

BDataIO* init_transport(BMessage *settings);
void exit_transport(void);

init_transport() is passed a BMessage object with a field printer_file of type B_STRING_TYPE containing the path to the spool folder. On success it returns a BDataIO object and NULL on failure. The BDataIO object can be used to write data to the printer.

The printer driver must not delete the BDataIO object. Instead it has to call exit_transport() and then unload the transport add-on.

Sample code can be found in class PrintTransport's method Open() in the folder src/add-ons/print/shared in the Haiku source code repository. The header file PrintTransport.h can be found in headers/private/print.

Printing the Print Job

Now we come to the core of a printer driver. This is the prototype of the function that is called by the print_server when the print job is ready to be processed:

BMessage* take_job(BFile* print_job, BNode* spool_folder, BMessage* settings);

The print job can be read using the file print_job. Its file format is explained below. Again the attributes of the spool folder can be accessed using spool_folder and the settings from the page configuration are also available, e.g. to get the page size.

To write data to the printer, the printer driver has to load the transport add-on and create a BDataIO object as explained in the section entitled Using a Transport Add-on.

To read the print job the class PrintJobReader from the Haiku source code repository can be used. This code snippet demonstrates how PrintJobReader can be used:

PrintJobReader reader(print_job);
if (reader.InitCheck() == B_OK) {
// the settings stored in the print job
BMessage* settings = reader.JobSettings();

// page number of the first page
int32 firstPage = reader.FirstPage();

// page number of the last page
int32 lastPage = reader.LastPage();

// paper and printable rectangle
BRect paperRect = reader.PaperRect();
BRect printableRect = reader.PrintableRect();

// resolution
int32 xdpi, ydpi;
reader.GetResolution(&xdpi, &ydpi);

int32 pages = reader.NumberOfPages();
// for each page
for (int page = 0; page < pages; page ++) {

PrintJobPage pjp;

if (reader.GetPage(page, pjp) == B_OK) {
BPicture picture;
BPoint point;
BRect rect;

// for each picture on page
while (pjp.NextPicture(picture, point, rect) == B_OK) {
// do some thing with the picture at point
}
}
}
}

What the printer driver does with the data from the print job is printer dependent and is not within the scope of this article. Sample source code can be found in the Haiku source code repository. For raster printer drivers see Haiku printer drivers Canon LIPS, PCL5, or PCL6. For "vector" printer drivers see the Haiku PDF printer driver.

Print Job File Format
struct  print_file_header {
int32 version;
int32 page_count;
off_t first_page;
int32 _reserved_3_;
int32 _reserved_4_;
int32 _reserved_5_;
};
This is declared in PrintJob.h. In the print job file, the header is followed by a flattened BMessage object containing the settings that are passed to take_job().
The print job file contains page_count page sections. The first starts at file offset first_page.

Page Section
struct  page_header {
int32 picture_count;
off_t next_page;
int32 _reserved[10];
};
This is followed by picture_count picture sections.
The next page section starts at file offset next_page.

Picture Section
struct _picture_header_ {
BPoint point;
BRect rect;
};
This is followed by a flattened BPicture object.

Removing a Printer

With the Printers preference application a printer can be removed. This deletes the printer folder from B_USER_PRINTERS_DIRECTORY if no pending print jobs exist.

Uninstalling a Printer Driver

A printer driver should be removed only if all printers have been removed with the Printers preflet. To remove the printer driver, the printer driver add-on has to be deleted from the system or user printer driver folder. There is little advantage to doing this — printer drivers are small and do not add to boot time or decrease system performance.

Updating a Printer Driver

Usually if a new version of a printer driver should be installed it is not necessary to remove the added printers from the system. In most cases it is sufficient to replace the printer driver add-on.

Issues to Consider When Writing a (Printer Driver) Add-On

A printer driver add-on is loaded on demand and usually unloaded as soon as it is not used any more. This means global states cannot be stored in global variables of the printer driver add-on. As mentioned already, the printer driver can store global states in an attribute of the spool folder.

It is also possible that multiple instances of the printer driver add-on are loaded and used at the same time (e.g. when the printer prints a print job and the user configures a page at the same time).

Multiple threads started by the printer driver can also be an issue. The printer driver has to ensure that all threads that have been started inside of the driver have exited before the driver returns from a function. E.g., when the printer driver opens a window for the configuration of a page in config_page(), a separate thread for the window is started. config_page() has to wait until the window is closed and the object that represents the window is completely destroyed, otherwise it could happen that the printer driver unloads the printer driver add-on while the window thread is still running. This will lead to a memory access violation because the window thread still accesses code in the add-on that is not loaded any more.