はじめに
次々に新しい技術が生み出され、超小型で非常に便利な最新の電子機器が続々と登場するこの世界で、プリンタは依然として重宝されています。人々は、まだ完全にソフトコピー(文書を電子的に複写したもの。PCなどの画面上で閲覧する)には移行していないのです。PCの画面をスクロールさせる代わりに、印刷された書類に目を通す理由はたくさんあります。たとえば、テクニカルライターの中には、文書の推敲にあたって、以前の内容のハードコピー(文書を紙に印刷したもの)にコメントを書き加えることにこだわる人がいます。
理由はどうあれ、人々は頻繁に印刷を行っています。多くの場合、印刷作業を調整したり、その場で印刷ジョブを変更したりすることが必要になります。こうした印刷処理の制御を行うには、多くの方法があります。その1つが、カスタムプリントプロセッサの利用です。
印刷ジョブのライフサイクル
図1は、印刷ジョブが通過する主要な段階――Windowsアプリケーションからの印刷要求に始まり、実際の印刷に至るまで――を表したものです。
ユーザーが文書をプリンタに送信することによって、この処理が開始されます。Windowsが導入された企業の環境では、通常、アプリケーションによって印刷ジョブがプリントサーバーに送られます。ネットワーク上のプリンタは、このプリントサーバーによって共有されています。Windowsの印刷スプーラサービスが印刷要求を受け取り、そのジョブをディスク上にスプールします。次に、プリンタに関連付けられたプリントプロセッサが呼び出され、印刷ジョブをプリンタ言語に変換します。その後、変換されたデータストリームを適切なプリンタポート(COM、LPTなど)に振り向けるという追加のタスクをプリントモニタが実行します。この実行にはCreateFile
、WriteFile
、ReadFile
、およびDeviceIOControl
というWin32 API関数が使われます。最後に、プリンタによって印刷された紙をユーザーが受け取るというわけです。

プリントプロセッサとは?
プリントプロセッサは、その名のとおり、プリンタにスプールされるジョブを処理します。プリントプロセッサは、Windowsアプリケーションとプリンタの間のレイヤと見なすことができます。このレイヤは、サポートされるデータタイプ(NT EMF、RAW、TEXTなど)からのジョブストリームを、適切なドライバを利用して特定のプリンタ言語に変換します。どんなプリンタにも、スプールされる印刷ジョブの処理を受け持つプリントプロセッサが割り当てられています。またプリントプロセッサは、印刷の中止、一時停止、再開のためのあらゆる要求の処理も担当します。
実は、プリントプロセッサはユーザーモードのDLLであり、Windowsの印刷スプーラサービスによって実行されます。Microsoftは、Driver Development Kit(DDK)の一部として、ドライバではないものの、サンプルコードを提供しています。このため、自作のプリントプロセッサを実装するのにドライバ開発の知識は必要ありませんが、コードの本質的な意味を理解するにはC/C++とWin 32のプログラミング経験が必要なことをご了承ください。
エクスポートされる関数
ここでは、Windows印刷スプーラとプリントプロセッサDLLの間のインターフェイスを紹介します。表1は、エクスポートが必要な6つの関数とその役割を説明するリストです。
関数名 | 説明 |
EnumPrintProcessorDatatypes | プリントプロセッサがサポートするデータの型を取得 |
GetPrintProcessorCapabilities | 指定されたデータ型についてのプリントプロセッサの処理能力を取得 |
OpenPrintProcessor | 印刷セッション開始時にプリントプロセッサをオープンし、そのハンドルを取得 |
PrintDocumentOnPrintProcessor | スプールされた印刷ジョブをプリンタ言語に変換 |
ControlPrintProcessor | 印刷の一時停止/中止/再開のための制御要求をプリントプロセッサに送信 |
ClosePrintProcessor | 印刷セッション終了時にプリントプロセッサをクローズ |
未知の領域:プリントプロセッサで何ができるか?
すべての印刷ジョブは必ずプリントプロセッサを経由するため、開発者は印刷処理をほぼ無制限に制御できます。先ほど説明したとおり、すべてのプリントプロセッサは、サポートされるデータ型をWindowsスプーラに通知する必要があります。各データ型は別々に処理され、制御のレベルはそれぞれ異なっています。たとえば、RAWデータ型はプリンタドライバ固有の形式でデータを表現します。RAWデータ型の印刷ジョブのコンテンツを調べるには、プリンタに特有のハンドラを書く必要があります。そのため、サポートされるプリンタの種類が制限されます。
幸いなことに、ほとんどのWindowsアプリケーションは、印刷ジョブをNT EMF(デバイス非依存の拡張メタファイル形式)でスプールします。EMFは文書化されており、印刷される実際のデータの確認が容易です。プリントプロセッサは、Windows GDIのAPIを利用してプリンタのコンテキストでEMFレコードを読み取ることによって、EMF形式のジョブをプリンタ言語に変換します。自作のプリントプロセッサを開発する場合は、GDI関数を自由に呼び出して印刷ジョブを変更できます。
プリントプロセッサは、あらゆる印刷ジョブの処理を、印刷ジョブを発行したNT/2000/2003ドメインアカウントのセキュリティのコンテキストを利用して行います。これは、コードの中でユーザーを識別したり、任意のセキュリティポリシーを強化できることを意味します。たとえば、Exchangeメールボックスのサイズが大きすぎるユーザーの印刷ジョブをブロックすることができます。プリントプロセッサは、セキュリティの点で非常に役に立ちます。単純にアクセスコントロールリストを定義するだけでは、このような複雑なパーミッションをプリンタに設定できないからです。
コーディングを始めるために必要なこと
まず、開発環境を構築する必要があります。
- Microsoft Visual Studio 7のインストール
- Windows DDK 2000(またはそれ以降のバージョン)のインストール
両製品とも、MSDNサブスクリプションのCD/DVDパッケージから入手できます。
プリントプロセッサDLLの開発は、DDKビルドツールをコマンドラインから利用するか、Visual Studio 6またはVisulal Studio 7のようなIDE(統合開発環境)を利用するかのどちらかの方法で行います。この記事のサンプルは、Visual Studio 7を利用して開発されたものです。
自作プリントプロセッサの実装方法
Microsoft DDKのサンプルを利用すれば、プリントプロセッサの自作は非常に容易です。この記事で紹介するコードは、DDKのgenprintをベースにしたものです。このサンプルはC言語で記述されていましたが、ここでは、オブジェクト指向モデルのメリットを活かすためにC++に書き換えてあります。
この記事で紹介するプリントプロセッサのサンプルは、カスタマイズ可能なコンテンツを持つセパレータページを印刷ジョブごとに追加で印刷するものです。コンテンツとして、1000x1000のビットマップ画像ファイル(C:\SeparatorPP\Separator.bmp)を使います。この画像ファイルを入れ替えれば、好みの画像をこのプリントプロセッサで使用できます。
それでは、サンプルのソースコードをじっくり見ながらその働きを理解していきましょう。表2は、ソースファイルの一覧です。それぞれに簡単な説明と、次のどのタイプにあてはまるかを記しています。
- DDK -- Microsoft DDKのサンプルをそのまま利用したファイルです。
- DDKを修正 -- Microsoft DDKサンプルのファイルを修正したものです。
- 新規作成 -- 新たにスクラッチから作成したファイルです。
ファイル名 | 説明 | タイプ |
SeparatorPP.def | 定義ファイルをエクスポート | DDK(ファイル名を「winprint.def」から変更) |
SeparatorPP.cpp | あらゆるプリントプロセッサがエクスポートすべき関数群を実装 | DDK(ファイル名を「genprint.c」から変更) |
local.h、local.cpp | デバッグ用の出力関数群 | DDK(「local.cpp」はファイル名を「local.c」から変更) |
support.cpp | プリントプロセッサのためのサポートルーチンを含む | DDK(ファイル名を「support.c」から変更) |
util.cpp | スプールメモリ管理用関数群 | DDK(ファイル名を「util.c」から変更) |
raw.cpp | RAWデータ型の印刷処理 | DDK(ファイル名を「raw.c」から変更) |
text.cpp | TEXTデータ型の印刷処理 | DDK(ファイル名を「text.c」から変更) |
parsparm.cpp | プリンタに対する改ページ文字の送信処理 | DDK(ファイル名を「parsparm.c」から変更) |
emf.cpp | NT EMFデータ型の印刷処理。このサンプルでは、NT EMFデータ型がセパレータページ追加の対象 | DDKを修正(ファイル名を「emf.c」から変更) |
EMFJobModifier.h | EMFジョブ修正のための抽象基底クラス(CEMFJobModifier)を定義 | 新規作成 |
JobSeparator.h、 JobSeparator.cpp | ジョブ修正クラスの定義と実装。ジョブ修正クラスは、CEMFJobModiferの派生クラスで、セパレータページの追加を行う | 新規作成 |
もともと純粋なC言語で書かれていたDDKサンプルをC++に変換したため、Cの拡張子が付いていたファイルはCPPの拡張子になっています。
新しいコードは、エクスポート関数PrintDocumentOnPrintProcessor
の中で実行されます。リスト1に、この関数(「SeparatorPP.cpp」内にあります)の標準的な実装を示します。
BOOL PrintDocumentOnPrintProcessor( HANDLE hPrintProcessor, LPWSTR pDocumentName ) { PPRINTPROCESSORDATA pData; /** Make sure the handle is valid and pick up the Print Processors data area. **/ if (!(pData = ValidateHandle(hPrintProcessor))) { return FALSE; } /** Print the job based on its data type. **/ switch (pData->uDatatype) { case PRINTPROCESSOR_TYPE_EMF_50_1: case PRINTPROCESSOR_TYPE_EMF_50_2: case PRINTPROCESSOR_TYPE_EMF_50_3: return PrintEMFJob( pData, pDocumentName ); break; case PRINTPROCESSOR_TYPE_RAW: return PrintRawJob(pData, pDocumentName, pData->uDatatype); break; case PRINTPROCESSOR_TYPE_TEXT: return PrintTextJob(pData, pDocumentName); break; } /* Case on data type */ /** Return success **/ return TRUE; }
ご覧のように、この関数は、処理の実行を印刷のデータ型に応じた適切なハンドラに委ねています。単純化のため、このサンプルではデータ型がNT EMFの印刷ジョブに限ってセパレータの追加を行っています。そのため、ここでは「emf.cpp」ファイル内のPrintEMFJob
関数に注目して説明していきます。この関数は、次の2つの引数をとります。
PPRINTPROCESSORDATA
-- ジョブデータへのポインタLPWSTR
-- 文書の名前
リスト2に、PrintEMFJob
関数のコードのうち、関連する部分だけを示します(コード全体については、ソースコードを参照してください)。
BOOL PrintEMFJob( PPRINTPROCESSORDATA pData, LPWSTR pDocumentName) /*++ Function Description: Prints out a job with EMF data type. Parameters: pData - Data structure for this job pDocumentName - Name of this document Return Value: TRUE if successful FALSE if failed - GetLastError() will return reason. --*/ { . . . // Get spool file handle and printer device context from GDI try { hSpoolHandle = GdiGetSpoolFileHandle(pData->pPrinterName, pDevmode, pDocumentName); if (hSpoolHandle) { hPrinterDC = GdiGetDC(hSpoolHandle); } } catch(...) { ODS(("PrintEMFJob gave an exceptionPrinter%ws\nDocument %ws\nJobID %u\n", pData->pDevmode->dmDeviceName, pData->pDocument, pData->JobId)); goto CleanUp; } . . . try { DocInfo.cbSize = sizeof(DOCINFOW); DocInfo.lpszDocName = pData->pDocument; DocInfo.lpszOutput = pData->pOutputFile; DocInfo.lpszDatatype = NULL; if (!GdiStartDocEMF(hSpoolHandle, &DocInfo)) goto CleanUp; bStartDoc = TRUE; //////////////////////////////////////////////////////////// // Adding Separator Page CJobSeparator JobSeparator(hPrinterDC); JobSeparator.ModifyPrintout(); // End of Adding Separator Page //////////////////////////////////////////////////////////// . . . } catch (...) { . . . } . . . SetLastError(LastError); return bReturn; }
コード中の太字で記された行が、セパレータページの機能をサポートするために修正した箇所です。NT EMFデータ型の印刷ジョブを処理する最初の部分に、プリンタのコンテキスト(hPrinterDC
)を取得する箇所があります。
hSpoolHandle = GdiGetSpoolFileHandle(pData->pPrinterName,
pDevmode,
pDocumentName);
if (hSpoolHandle)
{
hPrinterDC = GdiGetDC(hSpoolHandle);
}
このコードでは、セパレータページの追加を行うためにこのハンドルを使用します。PrintEMFJob
関数によって文書の印刷処理が開始された直後に、CJobSeparator
のインスタンスを作成して、印刷ジョブの修正を行います。
DocInfo.cbSize = sizeof(DOCINFOW); DocInfo.lpszDocName = pData->pDocument; DocInfo.lpszOutput = pData->pOutputFile; DocInfo.lpszDatatype = NULL; if (!GdiStartDocEMF(hSpoolHandle, &DocInfo)) goto CleanUp; bStartDoc = TRUE; //////////////////////////////////////////////////////////// // Adding Separator Page CJobSeparator JobSeparator(hPrinterDC); JobSeparator.ModifyPrintout(); // End of Adding Separator Page ////////////////////////////////////////////////////////////
実際のジョブ修正は、ModifyPrintout
関数内で行われます。この関数はCJobSeparator
クラス(リスト3を参照)に実装されています。
#include ".\jobseparator.h" #include <<TCHAR.H>> #include <<stdio.h>> #include <<io.h>> #define SEPARATOR_BITMAP_FILE _T("C:\\SeparatorPP\\Separator.bmp") bool CJobSeparator::ModifyPrintout() { HBITMAP hbmpSeparator = NULL; LPBITMAPINFO pBitmapInfo = NULL; try { hbmpSeparator = ::LoadImage(NULL, SEPARATOR_BITMAP_FILE, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_CREATEDIBSECTION); if (NULL == hbmpSeparator) { DWORD dwErr = ::GetLastError(); throw false; } BITMAP bm; ::GetObject(hbmpSeparator, sizeof(BITMAP), (LPVOID)&bm); pBitmapInfo = RetrieveBitmapInfo(hbmpSeparator, bm); if (NULL == pBitmapInfo) { // Unable to retrieve Bitmap Info throw false; } ::StartPage(m_hPrinterDC); // Stretching all the bitmap to Printer Device Context StretchDIBits(m_hPrinterDC, 500, 1750, 1000, 1000, 0, 0, bm.bmWidth, bm.bmHeight, bm.bmBits, pBitmapInfo, DIB_RGB_COLORS, SRCCOPY); ::EndPage(m_hPrinterDC); throw true; } catch(bool bRes) { if (NULL != hbmpSeparator) { ::DeleteObject(hbmpSeparator); } if (NULL != pBitmapInfo) { free(pBitmapInfo); } return bRes; } catch(...) { if (NULL != hbmpSeparator) { ::DeleteObject(hbmpSeparator); } return false; } }
StartPage
APIは、新しいページの処理を開始します。「C:\SeparatorPP\Separator.bmp」ファイルから読み込んだビットマップ画像はこのページに表示されます。ModifyPrintout
は、このビットマップをプリンタ装置のコンテキストに合わせて伸縮させる前に、ヘルパーメソッドRetrieveBitmapInfo
を使って、BITMAPINFO
構造体を作成します。最後に、EndPage
の呼び出しによって、セパレータページの印刷処理が終了します。
セパレータページの追加が終わると、PrintEMFJob
が、GDIのAPIを利用したEMFレコードの読み取りと、特定のプリンタ言語へのレコードの変換を行うことによって、印刷ジョブが完了します。
プリントプロセッサのインストール
プリントサーバーまたはローカルコンピュータに自作のプリントプロセッサをインストールするには、次の手順に従います(もちろん、アプリケーション用インストーラを作成すれば、この手順を自動化できます)。
- コンパイルしたDLLを、次に示すプリントプロセッサのフォルダにコピーします。
- 新しいレジストリキー「Separator」を既存の「HKLM\SYSTEM\CurrentControlSet\Control\Print\Environments
- 新たに作成した「Separator」キーに、ドライバのREG_SZ型変数を追加し、その値としてプリントプロセッサDLLの名前を指定します(この場合は、「Driver=SeparatorPP.dll」)。
- サービスアプレットまたは次のコマンドを用いて、Windowsスプーラサービスを再起動します。
- コントロールパネルの[プリンタとFAX]を選択し、使用するプリンタの[プロパティ]ウィンドウを開きます。
- [詳細設定(Advanced)]タブ(図2を参照)を選択し、[プリントプロセッサ...(Print Processor...)]ボタンをクリックします。表示されるリストから、自作のプリントプロセッサを選択します(図3を参照)。既定のデータ型はRAWのままにしておきます(この設定は、アプリケーションによってデータ型が指定されない場合にのみ関係します。大部分のNTシステムは印刷ジョブをEMF形式でスプールするため、そうなることは非常にまれです)。
%SYSTEMROOT%\system32\spool\prtprocs\w32x86
net stop spooler net start spooler


自作プリントプロセッサのデバッグ
すでに説明したように、プリントプロセッサはWindows印刷スプーラサービスによって実行されます。自作のアプリケーションをデバッグするためには、「spoolsv.exe」プロセスにデバッガをアタッチして、コードにブレークポイントを設定します。Microsoft Visual Studio 7を使用している場合、この操作は非常に簡単です(図4を参照)。
デバッグセッションを開く場合は、PDBファイルをプリントプロセッサのフォルダにコピーするのを忘れないでください。
ハードワークからハードコピーへ
それでは、文書を印刷して、自作のプリントプロセッサが正常に機能することを確認しましょう。「プリントプロセッサのインストール」での説明どおりにプリントプロセッサの関連付けを行ったプリンタで文書を印刷します。文書の印刷を行うたびに、「Separator.bmp」の画像が最初のページとして印刷されるはずです。
新しいアプリケーションの探索
比較的シンプルなこの技法を使えば、特定のプリンタでの印刷処理を制御することができます。この記事のサンプルコードまたはDDKのコードを利用すれば、不足機能の追加、印刷ジョブの変更など、お使いのプリンタの機能強化が可能です。ただし、自作コードのテストで紙を無駄使いするのは避けてください。テストには、バーチャルプリンタ、またはファイルへの印刷を利用するか、あるいは最低でもリサイクル紙を使って印刷するようにしましょう。