This could have been banner-node.jpg
May 2007
21
From Joshua

Bypassing the Windows Print Driver using DDK API Calls

Note: this is unbelievably old information. People entering college in 2007 have probably never used Windows 3.1. But this page still gets a lot of hits for some reason, so I keep it around.


Contents

Background

Microsoft Windows applications typically generate output via calls to a portion of Windows called the Graphics Device Interface (GDI). The GDI provides a consistent, device-independent method of generating text and graphics on any Windows-supported output device (such as video displays, printers, and plotters). By calling the GDI instead of the output device directly, the programmer no longer has to worry about what sort of monitor or printer the end-user. The programmer can use one set of source code that calls the GDI. The GDI, in turn, calls the appropriate driver, and the device driver issues the appropriate commands to generate the output.

Device-specific data

Sometimes it is necessary to send device-specific information to a printer or other device. For example, PCL files only work on HP and HP-compatible printers, and raw PostScript code will only work on PostScript-compatible printers. In these situations, device independence is not important — getting the proper output is.

The canonical method

The Microsoft-approved method for embedding device-specific data into a print stream is to use a passthrough block. This is done by issuing an Escape() command as follows:


short Escape(hdc, PASSTHROUGH, NULL, lpInData,NULL)

hdc HDC Identifies the device context.

lpInData LPSTR Points to a structure whose first word (16 bits) contains the number of bytes of input data. The remaining bytes of the structure contain the data itself.


Unfortunately, this does not always work as intended. There are bugs in the implementation of certain printer drivers that can cause the raw data to not be sent, or to send an erroneous page feed.

A workable solution

The GDI supplies functions used by the printer drivers and spooler for controlling print jobs. These functions are intended for use by the Windows Device Driver Kit (DDK), and are not documented as part of the normal Windows Software Development Kit (SDK). However, they exist in all versions of Windows so far (3.x, Win9x, NT, Win2K), and have not been deprecated by Microsoft. For C and C++ users, they are exposed in the DDK header file PRINT.H. The functions are OpenJob(), StartSpoolPage(), EndSpoolPage(), WriteSpool(), and CloseJob().

The DDK Documentation

The following functions work quite well for sending raw data to a printer. They have the advantage of working with the print spooler when the print job is being sent over a network or when the desired printer port is currently in use.

OpenJob

HANDLE FAR PASCAL OpenJob(lpOutput, lpTitle, hdc)

LPSTR lpOutput;

LPSTR lpTitle;

HDC hdc;

The OpenJob function creates a print job and returns a handle identifying the job. A driver uses the handle in subsequent functions to write output to the print job as well as control the job.

Parameters

lpOutput Points to a null-terminated string specifying the port or file to receive the output. A driver typically supplies the same filename as pointed to by the lpOutputFile parameter when GDI calls the driver’s Enable function.

lpTitle Points to a null-terminated string specifying the title of the document to print. This parameter must be supplied by the application when it calls the STARTDOC escape. This title appears in the Print Manager display.

hdc Identifies the application’s device context. This parameter must be supplied by the application when it calls the STARTDOC escape.

Return Value

The return value is a handle identifying the print job if the

The job was stopped because the application’s callback function returned FALSE (0).
SP_USERABORT (-3)The user stopped the print job by choosing the Delete button from Print Manager.
SP_OUTOFDISK (-4)A lack of disk space caused the job to stop. There is not enough disk space to create or extend the Print Manager temporary file.
SP_OUTOFMEMORY (-5)A lack of memory caused the job to stop

Comments

When Print Manager is not running, page division is not very important because temporary files are not involved. However, starting and ending at least one Print Manager page is still required. Calls to StartSpoolPage and EndSpoolPage can occur at any point during the output. Some drivers use one spool page per physical page. Others use one page for the whole job. The printing of a particular page by the Print Manager application does not begin until it receives the corresponding EndSpoolPage function. A driver can perform output at any point between these two calls. When EndSpoolPage is called and Print Manager is loaded, the page’s temporary file is submitted to Windows Print Manager.


WriteSpool

int FAR PASCAL WriteSpool(hJob, lpData, cch)

HANDLE hJob;

LPSTR lpData;

WORD cch;

The WriteSpool function writes printer output to the port or file associated with the print job. A driver must call this function after calling StartSpoolPage and before calling EndSpoolJob.

Parameters

hJob Identifies the print job. The handle must have been previously opened using the OpenJob function.

lpData Points to the device-dependent data to write.

cch Specifies the number of bytes to write.

Return Value

The return value is positive if the function is successful. Otherwise, it is one of the following error values.

ValueMeaning
SP_ERROR (-1)A general error condition or general error in banding occurred.
SP_APPABORT (-2)The job was stopped because the application’s callback function returned FALSE (0).
SP_USERABORT (-3)The user stopped the print job by choosing the Delete button from Print Manager.
SP_OUTOFDISK (-4)A lack of disk space caused the job to stop. There is not enough disk space to create or extend the Print Manager temporary file.
SP_OUTOFMEMORY (-5)A lack of memory caused the job to stop


EndSpoolPage

int FAR PASCAL EndSpoolPage(hJob)

HANDLE hJob;

The EndSpoolPage function marks the end of a spooled page. A driver uses this function, in conjunction with the StartSpoolPage function, to divide printer output into pages. Each page is stored in a temporary file on the machine’s hard disk when Print Manager is running. Dividing a print job into pages allows Print Manager to begin printing one page while the driver is still generating output on subsequent pages. A Print Manager page does not need to correspond to a physical page of printed output; the division is the driver’s decision.

Parameters

hJob Identifies the print job. The handle must have been previously opened using the OpenJob function.

Return Value

The return value is positive if the function is successful. Otherwise, it is one of the following error values.

ValueMeaning
SP_ERROR (-1)A general error condition or general error in banding occurred.
SP_APPABORT (-2)The job was stopped because the application’s callback function returned FALSE (0).
SP_USERABORT (-3)The user stopped the print job by choosing the Delete button from Print Manager.
SP_OUTOFDISK (-4)A lack of disk space caused the job to stop. There is not enough disk space to create or extend the Print Manager temporary file.
SP_OUTOFMEMORY (-5)A lack of memory caused the job to stop

Comments

When Print Manager is not running, page division is not very important because temporary files are not involved. However, starting and ending at least one Print Manager page is still required. Calls to StartSpoolPage and EndSpoolPage can occur at any point during the output. Some drivers use one spool page per physical page. Others use one page for the whole job. The printing of a particular page by the Print Manager application does not begin until it receives the corresponding EndSpoolPage function. A driver can perform output at any point between these two calls. When EndSpoolPage is called and Print Manager is loaded, the page’s temporary file is submitted to Windows Print Manager.


CloseJob

int FAR PASCAL CloseJob(hJob)

HANDLE hJob;

The CloseJob function closes the print job identified by the given handle.

Parameters

hJob Identifies the print job to close. The handle must have been previously opened using the OpenJob function.

Return Value

The return value is positive if the function is successful. Otherwise, it is one of the following error values.

ValueMeaning
SP_ERROR (-1)A general error condition or general error in banding occurred.
SP_APPABORT (-2)The job was stopped because the application’s callback function returned FALSE (0).
SP_USERABORT (-3)The user stopped the print job by choosing the Delete button from Print Manager.
SP_OUTOFDISK (-4)A lack of disk space caused the job to stop. There is not enough disk space to create or extend the Print Manager temporary file.
SP_OUTOFMEMORY (-5)A lack of memory caused the job to stop


C / C + + Examples

The functions are all prototyped in the standard header file PRINT.H, supplied with the device driver portion of the Windows Platform SDK. However, the DDK is not required to use these functions — you can declare them in your own code. The prototypes are as follows (substitute HANDLE for HPJOB if your are declaring them yourself):

Function Prototypes

HPJOB   WINAPI OpenJob(LPSTR, LPSTR, HPJOB);
int     WINAPI StartSpoolPage(HPJOB);
int     WINAPI EndSpoolPage(HPJOB);
int     WINAPI WriteSpool(HPJOB, LPSTR, int);
int     WINAPI CloseJob(HPJOB);

Gotchas

There are two gotchas. First, if you are #includeing PRINT.H, the prototypes are #defined to be invisible unless PRINTDRIVER is defined. Second, although the DDK documentation deals with HANDLEs, in the actual header file the prototypes are declared with HPJOBs. If you compile your application with STRICT defined and use HANDLEs or HDCs, you get compilation warnings.

The following code demonstrates how to use these functions to send raw data to the printer. It copies data directly to the printer, bypassing the printer driver. Please note this function is far from robust, and is only an example of what can be done.

Printing A File

Please note that some of this code is specific to C++ , such as the use of the new() operator, and declaring variables as needed. For straight-C coding, the functions work the same way.

#define PRINTDRIVER

#include <windows.h>
#include <string.h>
#include <print.h>
#include <stdio.h>

int PrintHPFile( const char *filename )
{
        char *pszPrInfo = new char[ 80 ];

        // Get the printer setup string from the ini file
        GetProfileString( "windows", "device", ",,,", pszPrInfo, 80 );

        // Format of ini file line = <Device,Driver,Port>
        char *pszDevice = strtok( pszPrInfo, "," );
        char *pszDriver = strtok( NULL, "," );
        char *pszPort = strtok( NULL, "," );

        // create a printer device context
        HDC PrintHandle = CreateDC( pszDriver, pszDevice, pszPort,
                        NULL );

        // get a job handle, allocate a buffer
        HPJOB PrintJob;
        char *buf = new char[ 1024 ];

        // open the file for reading
        FILE *hp_file = fopen( filename, "rb" );

        // Start the print job
        PrintJob = OpenJob( pszPort, filename, (HPJOB)PrintHandle );
        StartSpoolPage( PrintJob );

        // print the file, 1K at a time
        for( size_t i(0);; )
        {
                i = fread( buf, 1, 1023, hp_file );
                if( i>0 )
                        WriteSpool( PrintJob, buf, i );
                else
                        break;
        }

        // Close up print job
        EndSpoolPage( PrintJob );
        CloseJob( PrintJob );

        // Clean up
        DeleteDC( PrintHandle );
        fclose( hp_file );
        delete[] pszPrInfo;
        delete[] buf;
        return 1;
}

Code Explanation

The first chunk of code simply extracts information about the default printer and creates the device context needed by OpenJob().

char *pszPrInfo = new char[ 80 ];
GetProfileString( "windows", "device", ",,,", pszPrInfo, 80 );

char *pszDevice = strtok( pszPrInfo, "," );
char *pszDriver = strtok( NULL, "," );
char *pszPort = strtok( NULL, "," );

HDC PrintHandle = CreateDC( pszDriver, pszDevice, pszPort, NULL );

This is fairly canonical code, and hasn’t changed much since the days of Petzold. Next we create a print job handle, allocate a buffer, and open the file for reading.

HPJOB PrintJob;
char *buf = new char[ 1024 ];
FILE *hp_file = fopen( filename, "rb" );

The HDC help in PrintHandle is then used in the call to OpenJob(). For the other parameter to OpenJob() we use the device name extracted from the INI file, and use the passed in filename as the name of our print job. We then use the job number handed back to immediately start a spool page.

PrintJob = OpenJob( pszPort, filename, (HPJOB)PrintHandle );
StartSpoolPage( PrintJob );

The next few lines of code loop through the opened file, reading one kilobyte of data at a time, and using WriteSpool() to send the blocks of data to the printer. We continue doing this until fread() returns a 0, indicating an end-of-file condition.

for( size_t i(0);; )
{
        i = fread( buf, 1, 1023, hp_file );
        if( i>0 )

                WriteSpool( PrintJob, buf, i );
        else
                break;
}

Once the entire file has been sent to the printer, we end the print job by calling EndSpoolPage() and CloseJob(). All that is left is to free allocated memory, close the file, and release the DC we created.

EndSpoolPage( PrintJob );
CloseJob( PrintJob );

DeleteDC( PrintHandle );
fclose( hp_file );
delete[] pszPrInfo;
delete[] buf;

Merging Data

To merge data with an HP file, use WriteSpool() to send the HP file to the printer, then make multiple calls to WriteSpool() to “print” the data on the page. Hard-coded PCL commands would work here the same way they would in a DOS application. Note that EndSpoolPage() does not send a formfeed to the printer: those must be sent manually (again via a call to WriteSpool()). The following pseudocode gives an example of sending multiple pages. OpenJob()

FOR each page
        StartSpoolPage()
        WriteSpool() HP file to printer
        FOR each line of data to be printed, WriteSpool() to printer
        WriteSpool() a formfeed
        EndSpoolPage()
CloseJob()

Moving from DOS

Using WriteSpool() to send properly-formatted blocks of data and PCL code to the printer makes it quite easy to port the printing portions of routines from DOS to Windows. While this is normally not the preferred way for Windows applications to print data, printer-specific applications should notice a significant increase in both speed and programming ease using these DDK functions.


Visual Basic Examples

Declare Function OpenJob Lib "GDI" (ByVal lpOutput As String, ByVal lpTitle As String,
    ByVal hDC As Integer ) As Integer

Declare Function StartSpoolPage Lib "GDI" (ByVal hJob as Integer) as Integer

Declare Function WriteSpool Lib "GDI" (ByVal hJob as Integer, ByVal lpData As String,
    ByVal nSize As  Integer) As Integer

Declare Function EndSpoolPage Lib "GDI" (ByVal hJob as Integer) as Integer

Declare Function CloseJob Lib "GDI" (ByVal hJob as Integer) as Integer


Delphi Examples

Because the functions listed are DDK functions, they are not part of Delphi’s WinProcs unit. Here is the listing for a small unit that exposes the needed functions.

Function Prototypes

unit PrintAPI;
{$S-}

interface

uses WinTypes;

function OpenJob( PortName: PChar; FileName: PChar; PrinterDC: HDC ): HDC; far;
function StartSpoolPage( Job: HDC ): Integer; far;
function EndSpoolPage( Job: HDC ): Integer; far;
function WriteSpool( Job: HDC; StartAddress: PChar; DataLength: Integer ): Integer; far;
function CloseJob( Job: HDC ): Integer; far;

implementation

function OpenJob; external "GDI";
function StartSpoolPage; external "GDI";
function EndSpoolPage; external "GDI";
function WriteSpool; external "GDI";
function CloseJob; external "GDI";

end.

The following code demonstrates how to use these functions to send raw data to the printer. It copies a file directly to the printer without using GDI calls. It could be used for downloading fonts or for sending a form file to the printer. Please note that this function is far from robust: it assumes everything (memory allocation, DC creation, etc.) will work, and checks only for the existence of the file.

Printing a file

unit HPHelp;

interface

uses SysUtils, WinTypes, WinProcs, Messages, Printers, PrintAPI;

function PrintHPFile( filename: string ): integer;

implementation

function PrintHPFile( filename: string ): integer;
var
        DriverName: array [0..255] of char;
        DeviceName: array [0..255] of char;
        OutPut: array [0..255] of char;
        DeviceMode: Thandle;
        PrintHandle: HDC;
        PrintJob: HDC;
        hp_file: File;
        buff; pointer;
        FileLen; LongInt;

begin
        if FileExists( filename ) then
        begin
                  { Get printer info on global default printer }
              Printer.GetPrinter( DeviceName, DriverName, Output, DeviceMode );
                  { Create a device context for it }
              PrintHandle := CreateDC( DriverName, DeviceName, OutPut, nil );
                  { Hand DC to OpenJob, get Job# }
              PrintJob := OpenJob( OutPut, "HPFile", PrintHandle );
                  { Start a page on printer }
              StartSpoolPage( PrintJob );

                  { Read file into buffer }
              AssignFile( hp_file, filename );
              Reset( hp_file, 1 );
              FileLen := FileSize( hp_file );
              GetMem( buff, FileLen );
              BlockRead( hp_file, buff^, FileLen );
              System.CloseFile( hp_file );

                  { Write file out to page and end page }
              WriteSpool( PrintJob, PChar( buff ), FileLen );
              EndSpoolPage( PrintJob );
              CloseJob( PrintJob );
              FreeMem( buff, FileLen );

                  { Free up printer DC }
              DeleteDC( PrintHandle );
              PrintHPFile := 1;
        end
        else
        begin
              PrintHPFile := -1;
        end;
end;
end.

Code Explanation

The PrintJob() function requires a printer Device Context (DC). The easiest way to get one is to create one. We can use the global TPrinter object, Printer, to get the required info for the call to CreateDC() API function. TPrinter::GetPrinter() and CreateDC() are both documented in the Delphi on-line help file.

Printer.GetPrinter( DeviceName, DriverName, Output, DeviceMode );
PrintHandle := CreateDC( DriverName, DeviceName, OutPut, nil );

The HDC held in PrintHandle is used to call OpenJob(), which will start a print job. Everything sent to the print job until CloseJob() is called is treated as a contiguous block. The first parameter is the device name to be used, and is used internally by Windows to get device information from the WIN.INI or system registry. We use the device name provided by TPrinter::GetPrinter(). The second parameter is the name of the print job, and is used by Print Manager to identify the job.

PrintJob := OpenJob( OutPut, "HPFile", PrintHandle );

Once the job is opened, a call to StartSpoolPage() is made to open the print job for output.

StartSpoolPage( PrintJob );

The next few lines of code use Delphi functions (documented in the Delphi on-line help file) to load the complete contents of a file into memory.

AssignFile( hp_file, filename );
Reset( hp_file, 1 );
FileLen := FileSize( hp_file );
GetMem( buff, FileLen );
BlockRead( hp_file, buff^, FileLen );
System.CloseFile( hp_file );

The next three DDK calls send the entire block of data to the printer, end the page, and end the print job.

WriteSpool( PrintJob, PChar( buff ), FileLen );
EndSpoolPage( PrintJob );
CloseJob( PrintJob );

Finally, the allocated memory is released, and the DC is deleted.

FreeMem( buff, FileLen );
DeleteDC( PrintHandle );

Merging Data

To merge data with an HP file, use WriteSpool() to send the HP file to the printer, then make multiple calls to WriteSpool() to "print" the data on the page. Hard-coded PCL commands would work here the same way they would in a DOS application. Note that EndSpoolPage() does not send a formfeed to the printer: those must be sent manually (again via a call to WriteSpool()). The following pseudocode gives an example of sending multiple pages.

OpenJob()
FOR each page
        StartSpoolPage()
        WriteSpool() HP file to printer
        FOR each line of data to be printed, WriteSpool() to printer
        WriteSpool() a formfeed
        EndSpoolPage()
CloseJob()


References

Burk, Ron: Bypassing Printer Device Drivers. Windows/DOS Developer’s Journal. August 1995.

Burk, Ron: Bypassing Win95 Printer Device Drivers. Windows Developer’s Journal. February 1996.

Microsoft Corporation: Device Driver Adaption Guide. Windows 3.1 DDK. Document #PC29132-0392

Petzold, Charles: Programming Windows 3.1. Microsoft Press, Redmond, WA. 1992. ISBN 1-55615-395-3


Thanks to Hobie Audet (audet@micf.nist.gov) for pointing out some errors in the C++ code.

You may also want to check out Perilous Software’s RAWPRINT, which does exactly what this document talks about. Thanks to Gary Goubert (pontiac@pacbell.net) for pointing me in the right direction.