Home » MSI (Windows Installer) » Packaging a PDF Printer Driver

Packaging a PDF Printer Driver

Posted on by

I’ve been working on a PDF printer driver recently.  It was installed as part of another product – a crumby old InstallShield setup.exe that didn’t install or uninstall silently (and response files didn’t work).  As such it required repackaging.

I couldn’t locate an INF file for the printer driver, so I had to cobble one together (more on that another time) and sign the driver myself.  Once I did this I then had to install it and remove it cleanly.  To accomplish this, I used the following script inside an elevated Custom Action (Deferred in a System Context) and sequenced just before the InstallFinalize standard action:

Option Explicit 

'if running on x64 OS, PNPUtil.exe only exists in System32 (not SysWOW64) so ensure that this custom action
'runs in x64 mode.  Or change the path to PNPUtil.exe to use SysNative.

'define variables since we use Option Explicit 
dim wshShell : Set wshShell = CreateObject("Wscript.Shell")
dim fso : Set fso = CreateObject("Scripting.FilesystemObject")
dim value, i, publishednameval, driverpackageproviderval, classval, driverdateandversionval, signernameval

'********************************
'specify the printer options below
'********************************

'printer name as it will appear in printer queue
dim printerName : printerName = "Black Ice PDF"
'driverName value from INF
dim printerDriverNameInf : printerDriverNameInf = "Black Ice PDF Driver"
'DriverVer value from INF
dim printerDriverVerInf : printerDriverVerInf = "06/19/2019,15.25.0.0"
'Driver Manufacturer value from INF
dim printerDriverManufacturerInf : printerDriverManufacturerInf = "Black Ice Software LLC"
'Port name
dim portName : portName = "IcePortPUR:"
'Path to INF file
dim pathToInf : pathToInf = Session.Property("CustomActionData") & "\BlackIcePDFDesktop.inf"

'********************************
'specify whether you are adding or removing the printer
'********************************

'if we want to add the printer, uncomment add_printer and comment remove_printer.
add_printer printerName,printerDriverNameInf,pathToInf,portName

'if we want to remove the printer, uncomment remove_printer and comment add_printer
'remove_printer printerName, printerDriverNameInf,printerDriverVerInf,printerDriverManufacturerInf

'********************************
'do not edit below here
'********************************

'This function splits each line of the PNPUtil.exe output.
'We're specifically trying to locate the name of the OEMxx.INF file
function get_pnputil_value(pnputilentry)

	value = ""
	if instr(pnputilentry,":") > 0 then
		value = split(pnputilentry,":")(1)
	end if
	value = trim(value)
	get_pnputil_value = value
	
end function

'removes printer, printer driver and removes it from Driver Store
function remove_printer(printerName, driverName, driverVersion, drivermanufacturer)
	
	'replace comma in INF driver version
	driverVersion = replace(driverVersion,","," ")

	'remove printer from queue - /q will hide any prompts so remove whilst debugging
	wshShell.Run "RUNDLL32 printui.dll, PrintUIEntry /dl /n " & chr(34) & printerName & chr(34) & " /q", 0, true

	'remove driver - /q will hide any prompts so remove whilst debugging
	wshShell.Run "RUNDLL32 printui.dll, PrintUIEntry /dd /m " & chr(34) & driverName & chr(34) & " /q", 0, true

	'now remove from driver store
	dim strCmd : strCmd = "cmd /q /c for /f ""skip=2 tokens=*"" %a in ('pnputil.exe -e') do @echo %a"
	dim drivers : drivers = wshShell.Exec(strCmd).StdOut.ReadAll()

	'split output by new line
	dim alldrivers : alldrivers = split(drivers,VbCrlf)

    'loop over each line in PNPUtil.exe output
	for i = 0 to Ubound(alldrivers) -1 Step 5
		publishednameval = get_pnputil_value(alldrivers(i))
		driverpackageproviderval = get_pnputil_value(alldrivers(i+1))
		classval = get_pnputil_value(alldrivers(i+2))
		driverdateandversionval = get_pnputil_value(alldrivers(i+3))
		signernameval = get_pnputil_value(alldrivers(i+4))

        'if manufacturer and version matches ours, remove from Driver Store
		if (driverpackageproviderval = drivermanufacturer and driverdateandversionval = driverVersion) then
			wshShell.Run "pnputil.exe -f -d " & publishednameval, 0, true
		end if
	next

	'stop spooler
	wshShell.Run "NET STOP Spooler", 0, true

	'start spooler
	wshShell.Run "NET START Spooler", 0, true

end function

function add_printer(printerName, driverName, pathToInf, portName)

	if fso.FileExists(pathToInf) Then
	
        'Either create a port (if required) here using printui.dll, or (in our case) the port
        'is created via the windows registry and related files.
	
		'stopping and starting spooler first will ensure that our new port is available

		'stop spooler
		wshShell.Run "NET STOP Spooler", 0, true

		'start spooler
		wshShell.Run "NET START Spooler", 0, true

		'add printer - /q will hide any prompts so remove whilst debugging.  /y to set it as default.
		wshShell.Run "RUNDLL32 printui.dll,PrintUIEntry /if /b " & chr(34) & printerName & chr(34) & " /f " & chr(34) & pathToInf & chr(34) & " /r " & chr(34) & portName & chr(34) & " /m " & chr(34) & driverName & chr(34) & " /q", 0, true
	end if
	
end function

Set wshShell = Nothing
Set fso = Nothing

Configuring the Script

  1. This script is used to install AND remove printers, by commenting out the appropriate line (see script comments).
  2. To get the pathToInf value I use a ‘Set Property’ custom action to pass the location of the INF file to my deferred custom action.  So in essence I ended up with 2 VBScript custom actions (for install and remove) and 2 Set Property custom actions (for install and remove).  If you wanted to be lazy you could always hard code the path.
  3. On x64 platforms, you MUST set the msidbCustomActionType64BitScript flag on the Custom Action because PNPUtil.exe only exists in the System32 folder and NOT the SysWOW64 folder.

Creating the Printer Port

There are a few ways to create a printer port, depending upon your requirements.  You could create an entry in HKLM\Software\Microsoft\Windows NT\CurrentVersion\Ports and stop/start the print spooler.  You could also use PrintUI.dll to create a printer port.

In this example, the printer port is created by a combination of registry and files put down by the Windows Installer:

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Monitors\Ice Monitor P]
"Driver"="BuPMonNT.dll"

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Monitors\Ice Monitor P\PortList]
"IcePortPUR:"=" "

We can see that the driver DLL is called BuPMonNT.dll, which resides in the System32 folder.  Not only does this file need to exist for the port creation to work, but the dependencies of this DLL need to exist too.  Using Dependency Walker to view BuPMonNT.dll we can see that one dependency was missing (PDF32.DLL – there were several other dependencies required too).  Once we added this to System32, added the aforementioned registry and stopped and started the printer spooler, the printer port registered successfully during the script execution.

printer-driver-dependencies

Comments have now been disabled. If you have a question to ask about this post please ask the community!