Download PDF: Bitmap Driver

Bitmap Driver for Windows NT 4.0

Architecture of the Print Subsystem

The Windows NT Printing architecture is a modular system, which allows vendors to customize the component in order to integrate their specialized components into the system. The Printing architecture is shown below.

Print Subsystem Architecture

The actual flow of control through the printing subsystem from the moment the application gives a print call till the user obtains the print output is as given below:

  • Application makes win32 printing and GDI calls, these are primarily functions that are found in Winspool.dll and gdi32.dll. These calls are device independent calls.
  • The Win32 calls are passed to the Kernel - Mode GDI rendering engine. The printer driver in conjunction with the graphics-rendering engine renders the input and passes the raw data on to the spooler.

    The Printer driver is a specific example of a graphics driver in the Windows NT operating system. Another example would be the monitor driver. GDI communicates with the graphics drivers through a set of DDI functions. Information is passed between the GDI and the driver through the input/ output parameters of these entry points. The driver supports the GDI's requests by performing the appropriate operations on the associate operations before returning the GDI call. GDI has a lot of output capabilities built into it, the driver can thus make use of these facilities instead of it actually providing them. These are exported by GDI in the form of certain services. This way GDI actually reduces the functionality that needs to be implemented by the driver as well as the actual size of the driver.

  • The spooler further processes the data and sends it to the port driver to which the target printer is connected. In case the system also has a language monitor then the data passes through the language monitor before reaching the port monitor and then onto the actual print device.

Click here to go to Top

Capturing a print job: The Mechanics of what is to be done

In order to capture a print-job, we do not write one single executable, but make a series of changes to the printing subsystem. We develop the following for this

  • Printer MiniDriver
  • A code plugin into the Microsoft Renderer
  • Port Monitor

Developing each of these is a separate task by themselves. They are explained in the subsequent sections.

The flow of logic through the system is pretty straightforward.

A printer minidriver is written for our custom printer in whom we specify the escape sequences for the printer. When the print job is initiated by the application the application makes GDI calls to the GDI rendering engine. The engine with the help of the printer driver file then generates the raw job. The printer driver (of which the data file is a part) provides the information about the printer, like the resolutions supported by the printer, paper sizes etc.

The Rendering engine actually renders the print-job to form scanlines out of them. Microsoft also provides a callback called CBFilterGraphics that can be used to capture these scanlines before they are actually sent to the spooler. Hence the user is free to modify them before they are sent to the spooler. In our system we compress the actual scanlines (ordinary RLE compression) and add a starting and ending tag to this so that we can later recognize the beginning at the ending of the scanline.

The next stage where we interact with the scanlines is at the port monitor. In the port monitor we do not have direct access to the scanlines. The only visible data is the print job, which is a combination of both the data and the escape sequences. Escape sequences are certain commands that are given to a printer for it to perform certain operations. For e.g. the Diconix 150 printer understands the sequence \x1B*b1M to mean that the data (after the command has been received) is in the RLE format. Different printers have different escape sequences. The printer has built into it a mechanism that is capable of interpreting the escape sequences and responding to it. However, Here we trap the scanlines using the tokens we have inserted into the job and store them onto a file and voila we have a bitmap.

Click here to go to Top

Creating a Printer Mini Driver

A printer minidriver is responsible to supply information about the printer to the rendering engine. The renderer uses this information to decide how the rendering of the print job needs to be done. Printer minidriver is created using a tool provided with the Microsoft Device Driver Development Kit called the Unitool.

The Windows NT Unitool is a development tool that allows driver writers to easily create a minidriver for their raster printer. It permits GPC data to be organized and defined into a minidriver for a single raster printer or a group of similar raster printers. Unitool automatically combines the printer description file with a layer of RasDD interface code to create a minidriver. The General Printer Charecterization (GPC) data that Unitool creates in the minidriver provides the resources that RasDD requires to run a printing device. This eliminates the need to create a complex driver for every printer within a family of similar printing devices.

Unitool, sample GPC data, and other related files and information required to build a minidriver are provided in the Windows NT DDK. Sample GPC data, used to create actual Windows NT minidrivers, is provided for the following printer drivers:

  • Digital Equipment Corporation driver for DEClaser 3200 printers
  • Epson 9-pin drive
  • Epson 24-pin driver
  • Hewlett Packard driver for printers that use HPPCL
  • HP PaintJet driver
  • A text-only base driver

To save development time, driver writers can start with the supplied GPC data set that most closely resembles the characteristics of their raster printer, and customize it to fit their device. In order to create a Bitmap printer we take the simplest minidriver which is the Dec3200 driver. We modify this as explained to suit our needs.

The GPC resource data in Unitool enables you to collect comprehensive information about the related printer driver in the minidriver. Each minidriver supports RasDD and the RasDD user interface. For the user interface, the minidriver data defines general information about the printer, such as the number of available paper trays, and what form is contained in which tray.

For Our Printer Driver the only significant information that we are interested in the Minidriver is given below:


Supported Resolutions: 75, 150, 300 DPI

Other Flags and paramenters in the Resolutions Dialog Box
















Another slight change that needs to be done is in the Cursor Movement feature. In the Cursor movement dialog box the user sets a value for the CM_OCD_YM_REL parameter. This parameter gives the relative Y distance of the cursor from the starting of the page. In this for the CM_OCD_YM_REL we specify the value as "~%d~". The significance of this is very important. By default Windows NT does not call the code plugin (explained in the next section) as specified for every scanline. In particular id does not call this for every blank scanline. So in case we form a bitmap from just the scanlines that we have trapped in this plugin, we would not be able to form the bitmap completely. The bitmap would not contain in it the blank lines in the original print job (I had this problem initially). To overcome this and get the proper image we need the scanlines corresponding to the blank scan lines too. This is where the CM_OCD_YM_REL parameter helps us. In this we set we find that NT specifies the number of blank sca nlines that need to be generated to the printer. So in case we find a value like "~40~ " in the spool file then it means that we need to add proportionate blank lines to the image.>

All other parameters are set to their default values.

Click here to go to Top

Code plugin into the Microsoft Renderer

Windows NT provides a registered callback called CBFilterGraphics that gets control of the scan lines after they have been rendered and before they are sent to the spooler. The User is free to modify the scanline here. Once important use of this callback would be to compress the scanlines that are rendered into the format we desire (Say RLE). The same applies to encrypting the scanlines as well. This is more in the case of a network printer where in we need to send the scanlines across the network. In such a system we could ensure security by simply encrypting the scanlines by any standard algorithm (say DES) ans send it across the network. The callback gets control for every scanline that is generated.

The prototype of the functions is as follows:

Address of the private buffer used by this function. This buffer must not be altered.

Address of the raster buffer that holds data.

Size in bytes of the buffer specified by lpBuf

In our bitmap printer driver we add a beginning and an ending tag to be able to identify the scanline. In particular we add the tokens "#$" in front and "^*" at the end. This way all the data bytes between these two tokens correspond to the scanline. This makes it easy to identify the scanline in the print job.

Another interesting technique to capture the print job would be to write the scanlines into a file in this function. But however, the problem that we face here is twofold

  • We do not get blank scanlines here
  • We don't get the escape sequences of the printer.

These two factors make it impossible for us to create the BMP directly at this stage. However in systems like Windows 95 these problems are not present and hence it is possible to capture the scanlines and create a BMP in the callback directly (The name of the callback in windows95 is fBlockOut).

Click here to go to Top

A Small Note on Generating Color Bitmaps

In order to create color bitmaps, the creation of the minidriver is slightly modified. At the outset the first change that we need to make is to add a color model feature to out minidriver. This is done by selecting printerData->graphics Info->colormodels->Add.

Once we have a color model, unitool allows us to define the number of bits for each pixel. We also enable the DC_PRIMARY_RGB flag in the fGeneral dialog box. Once this is done click OK. Incase of True color specify sBitsPerPizel to be 24 and in case of Gray specify that to be 8. For Monochrome bitmaps we don't need a color model feature in the driver. Though the documentation says that DMCOLOR library allows for only 8 colors, we find that it actually does support upto 24 bits. Infact Unitool would report and error when we select 24 bits. We just choose to ignore the error that it reports. Once all these data have been given Windows NT renders the image to form a RGB image.

Click here to go to Top

Port Monitor: The Key to success

A port monitor is responsible for the communication channel between the spooler and the printer device. It supports one or more types of communication channels. For example, the Windows NT default port monitor, localmon, supports parallel, serial, and file ports.

To be able to print to a physical printer, Windows NT requires that the printer be connected to one or more ports that are controlled by port monitors. A port represents the destination for the data being sent to the printer.

Writing a port monitor

A port monitor is an integral part of the Windows NT printing architecture. Every port monitor supports a standard set of APIs. The functions that need to be supported by the port monitor are as follows:

  • InitializePrintMonitor
  • DllEntryPoint
  • OpenPort
  • OpenPortEx
  • ClosePort
  • StartDocPort
  • WritePort
  • ReadPort
  • EndDocPort
  • AddPort or AddPortEx
  • DeletePort
  • EnumPorts
  • ConfigurePort
  • SetPortTimeOuts
  • GetPrinterDataFromPort

The spooler at various instances calls these functions. Each of these functions has specialized functionality and is explained below.


The spooler calls this function at the times of its initialization. The spooler obtains from this function the monitorex structure, which holds the entrypoints for the other functions. The Port Monitor has only two entry points. One of them is this function and the other is the dllentrypoint. The port monitor, in the monitorex structure, exports all other functions.


This function is called when the spooler loads the Port Monitor through the LoadLibrary win32 API function. Other than serving as the standard entry point for the spooler to load the DLL into memory the functions does not do anything else.


The spooler calls OpenPort whenever a port is assigned to a printer. It returns a handle to the port identified by pName. The spooler uses the returned port handle in subsequent calls to the port monitor's StartDocPort, WritePort, ReadPort, and EndDocPort routines. The spooler waits for OpenPort to return, so a monitor should return success or fail within a reasonable amount of time. All initializations that the port monitor may have to do are performed by this function. To avoid hung or unreasonably delayed port responses the port monitor should always establish reasonable timeouts before actually calling a port.


This function needs to be provided only by Language monitors. Hence we will not be dealing with this function here.


The spooler calls ClosePort when a monitor other than the current one should hold the port, or when no printers are connected to the port specified by hPort. The spooler calls ClosePort when no printers are connected to the port specified by hPort.


StartDocPort performs the tasks required to start a print job on the specified port. The spooler calls StartDocPort when it is ready to send a job to the printer. StartDocPort should perform whatever port setup is required in order to send data from subsequent WritePort calls to the printer.


WritePort sends the data pointed to by pBuffer to the printer. The spooler calls WritePort as often as is necessary to send an entire print job to the printer. The spooler sets the block size specified by cbBuf, so a monitor should not make any assumptions about it. If there is no response from the printer, WritePort should wait a reasonable amount of time and then returns FALSE. WritePort should always initialize pcbWritten to zero before attempting the port write. If the port write succeeds, pcbWritten should be overwritten with the number of bytes sent. Note that a port can return zero bytes on a successful write; WritePort should return TRUE in this situation.

Click here to go to Top


ReadPort supports bidirectional printers. If the printer provides any data, this data is written to the buffer pointed to by pBuffer. If there is no response from the printer, ReadPort waits long enough to ensure there is no data coming and then returns FALSE. ReadPort should always initialize pcbRead to zero before attempting the port read. If the port read succeeds, pcbRead should be overwritten with the number of bytes read. Note that a port can return zero bytes on a successful read; ReadPort should return TRUE in this situation.

A timeout could be specified by calling a port monitor's SetPortTimeouts routine.


The printer calls EndDocPort after it has finished with the job of sending the data by calling writeport. Monitors should call the Win32 SetJob function to inform the spooler that a print job is completed. A port monitor's EndDocPort routine should call SetJob with the dwCommand parameter set to JOB_CONTROL_SENT_TO_PRINTER. When a print job goes through a language monitor, spooler ignores any notifications that it receives from the port monitor. Consequently, a monitor that can determine true end-of-job status should delay calling SetJob until the printer notifies it that the print job has been completely printed. A language monitor's EndDocPort routine can setup to listen for end-of-job notification from the printer. This monitor should pass JOB_CONTROL_LAST_PAGE_EJECTED when it does receive notification from a bidirectional printer that printing has been completed. Monitors might wish to modify their behavior when a user deletes or restarts a print job. To determine wheth er this has occurred, monitors can call the Win32 GetJob function and check the job status for the JOB_STATUS_DELETING or JOB_STATUS_RESTART flags. EndDocPort should also free any per-print-job resources that were allocated by StartDocPort.


AddPort creates a port and adds it to the list of ports currently supported by the specified monitor in the spooler environment. AddPort allows a port to be added interactively. A monitor should prompt a user to enter the port name in a dialog box on the window associated with hWnd. AddPort should validate the entered port name by calling the Win32 EnumPorts function to ensure that no duplicate port names are added to the spooler environment. A monitor should also verify that the port is one that it supports. The spooler does not support remote AddPort calls. Consequently, AddPort implementations can ignore the pName and pMonitorName parameters.


The spooler calls DeletePort to delete a port from the monitor's environment. The monitor should delete the specified port from its state. The spooler will not call DeletePort on a monitor as long as a port is open. Applications can delete local and remote ports. The printer UI displays a confirmation message box before the spooler calls DeletePort, so a monitor should ignore the hWnd parameter and not display another dialog box.


The spooler calls EnumPorts to get the list of ports maintained by a monitor. At spooler initialization, the spooler calls EnumPorts for all installed print monitors to build up a list of all available ports. The spooler expects valid results from EnumPorts regardless of whether a printer has yet been defined. If a monitor doesn't support the structure specified by Level, EnumPorts should log an ERROR_INVALID_LEVEL error and fail.

If cbBuf is too small, EnumPorts should set ERROR_INSUFFICIENT_BUFFER, and return in pcbNeeded the number of bytes required to copy all of the data. If cbBuf is large enough, EnumPorts should return the number of bytes copied into pPorts.

EnumPorts must write the PORT_INFO_Xx structure(s) at the beginning of the pPorts buffer, and the string(s) associated with the PORT_INFO_Xx structure(s) at the end of the buffer. Monitors that do not pack data in this way will cause Win32 EnumPorts calls to the spooler to fail. See the localmon code for an example of how to pack the pPorts buffer. Monitors should set the pMonitorName field of the PORT_INFO_2 structure to be a string that describes their monitor. It is the monitor's responsibility to localize this string. Monitors should set PORT_INFO_2's fPortType field to zero. EnumPorts is a port management function that must be implemented by port monitors.


The spooler calls ConfigurePort to perform port configuration. ConfigurePort can offer a dialog box to obtain some or all of the necessary port configuration information from the user. The spooler does not support remote ConfigurePort calls; consequently, monitors can ignore the pName parameter. ConfigurePort is a port management function that must be implemented by port monitors


This is an optional function and hence has been ignored.


This is an optional function and hence has been ignored.

The Windows NT DDK has a sample Port Monitor in the ddk\src\print\localmon directory. Using this sample the necessary changes could be made.

Once these functions have been implemented, we have a raw port monitor with us. We make a few modifications in this port monitor for it to suit our requirements.

The Modifications are outlines below:

  • In StartDocPort

    Create a File and open it in the write mode. Into this file we'll store the actual print job.

  • In WritePort

    Write all the scanlines that we obtain into the File that was created in StartDocPort. This ensures that all the print data is captured. Moreover whenever we encounter a "~%d~" in the print job we add blank scan lines to the File into which we dump the data. The number of blank scanlines that are added is exactly (%d*Resolution)/MasterUnits. MasterUnits is the least common multiple of the various horizontal and vertical resolutions supported by the printer.

  • In CloseDocPort

    Close the file that we created in StartDocPort. This file now contains all the print job data in it. Call a Function (say ProcessData ()) after this. This function is responsible for the creation of the BMP. The details of this function are explained below.

ProcessData (): Unmasked

This function is responsible for converting the print meta-data into a Bitmap. Before seeing how this function converts the file into a bitmap let us have a cursory look of what material a bitmap constitutes of a BITMAPFILEHEADER and a BITMAPIUNFOHEADER followed by a required number of RGBQUAD structures and then the data as such.. We assume the reader is familiar with the file format of a BMP, which is clearly documented.

So to create our BMP we need to get the following:

Information on the

  • DATA


This structure consists of the following:

  • bfType

    This is always set to BM. This is the signature for the bitmap file.

  • bfSize

    This contains the Size in bytes of the file.

  • bfReserved1

    Always set to Zero.

  • bfReserved2

    Always set to Zero.


This structure consists of the following:

  • biSize

    Size of the Header in bytes, which is 40 bytes.

  • biWidth

    Size of the image width in pixels. This corresponds to the length of the scanline. In our case the length of the paper in pixels corresponds to the length of the Scanline. Since we preset the size of the paper during the print operation we have with us the paper dimensions and hence biWidth.

  • biHeight

    This is the Length of the paper. This is also obtained from paper dimensions explained above.

  • biPlanes

    Number of image planes. This is always set to 1.

  • biBitCount

    This holds the number of bits that are allocated to each pixel. This is also obtained as we set the number of bits in the printer data file, we obtain this data as part of the print job.

  • biCompression

    This is a flag that to inform whether the BMP is compressed or not. The BMP we create is not compressed ans hence the flag is set to zero.

  • biSizeImage

    This holds the size of the compressed image in bytes. Since we do not compress the image, this is set to zero.

  • biXPelsPerMeter

    Horizontal resolution of the image. We set the horizontal resolution in the printer data file, so we have the value here.

  • biYPelsPerMeter

    Vertical resolution of the image. We set the vertical resolution in the printer data file, so we have the value here.

  • biClrUsed

    Corresponds to the number of colors have been used in the BMP.

  • biClrImportant

    Corresponds to the number of colors that are important. In our case it is the same as the biclrused parameter.


This corresponds to the number of color maps in the file. For each color that is there in the BMP we add a color map.hence for 2 colors we have 2 RGBQUAD structures for 8 we have 255 structures. However for 2^24 we have no color maps at all as it is not feasible to have so many of them.


The BMP data corresponds to the data that we have stored in our file created in StartDocPort. We dump the entire data into this new file.

La-Voila we now have a BMP stored on disk!

Click here to go to Top


This system is capable of storing only data that is sent from the bitmap driver. The performance of the system though depends on the bitmap that is being generated. With increase in the DPI that is needs the size of the BMP (ofcourse!) increases and hence the time taken to create the BMP also increases. This is the same for the generation of color and grayscale bitmaps.

T (Monochrome) < T (GrayScale) < T (Color)

Ideally it is better to create compressed bitmaps (say RLE) rather than creating uncompressed bitmaps. This way we can ensure that size of the BMP is not too big and hence the time taken for creation is also substantially minimal. In general the time taken to create the BMP is proportional to the size of the bitmap to be generated.

This system is not capable of storing the data from other printers. This is more a technology exploration area than a separate system though.

Click here to go to Top


Windows NT Device Driver Kit Documentation

Click here to go to Top